Summary
MSBuild's -mt mode crashes when a task returns a String[] containing null elements. The BinaryTranslator serialization code doesn't handle null array elements.
Stack Trace
System.ArgumentNullException: Value cannot be null. (Parameter 'value')
at System.IO.BinaryWriter.Write(String value)
at Microsoft.Build.BackEnd.BinaryTranslator.BinaryWriteTranslator.Translate(String[]& array)
at Microsoft.Build.BackEnd.TaskParameter.TranslatePrimitiveTypeArray(ITranslator translator)
at Microsoft.Build.BackEnd.TaskHostTaskComplete.Translate(ITranslator translator)
at Microsoft.Build.BackEnd.NodeEndpointOutOfProcBase.RunReadLoop(...)
Reproduction
Any task returning String[] with null elements crashes in -mt mode:
public class MyTask : Task
{
[Output]
public string[] Results { get; set; }
public override bool Execute()
{
// Pre-allocated array not fully filled
Results = new string[10];
Results[0] = "first";
// Results[1..9] are null - CRASH in -mt mode
return true;
}
}
Risk Assessment
High risk for custom tasks - Common patterns trigger this:
- Pre-allocated arrays not fully filled
- LINQ projections:
items.Select(x => x?.Value).ToArray()
- Sparse arrays for optional results
A String[] with null elements is valid .NET; MSBuild should serialize it correctly.
Fix plan proposal: TaskHost should correctly serialize string[] containing null elements
Problem Statement
MSBuild's -mt mode TaskHost crashes when a task returns a String[] containing null elements. The BinaryTranslator serialization code calls BinaryWriter.Write(null) which throws ArgumentNullException.
Stack Trace:
System.ArgumentNullException: Value cannot be null. (Parameter 'value')
at System.IO.BinaryWriter.Write(String value)
at Microsoft.Build.BackEnd.BinaryTranslator.BinaryWriteTranslator.Translate(String[]& array)
at Microsoft.Build.BackEnd.TaskParameter.TranslatePrimitiveTypeArray(ITranslator translator)
at Microsoft.Build.BackEnd.TaskHostTaskComplete.Translate(ITranslator translator)
Real-World Impact: The SDK's GenerateGlobalUsings task and similar tasks can trigger this bug. When building aspnetcore with -mt mode, TaskHost worker processes crash.
Analysis Confirmed
Why TaskHost is affected but normal worker nodes are not
-
TaskHost path (VULNERABLE): Task outputs are serialized via TaskHostTaskComplete → TaskParameter → BinaryTranslator.Translate(ref string[] array). This directly calls BinaryWriter.Write(array[i]) without null checking.
-
Normal worker path (SAFE): Task outputs flow through in-process IBuildEngine callbacks that filter/convert values before any binary serialization occurs. The raw task output objects with null array elements never reach the serialization layer.
Root Cause
In BinaryTranslator.cs, the BinaryWriteTranslator.Translate(ref string[] array) method (lines 1159-1173) iterates over the array and calls _writer.Write(array[i]) without checking for null elements:
for (int i = 0; i < count; i++)
{
_writer.Write(array[i]); // BinaryWriter.Write(null) throws!
}
Same issue affects:
List<string> (lines 1189-1192)
HashSet<string> (lines 1206-1208)
Proposed Solution
Add null element handling by writing a boolean marker before each string element:
Write side:
for (int i = 0; i < count; i++)
{
string element = array[i];
bool hasValue = element != null;
_writer.Write(hasValue);
if (hasValue)
{
_writer.Write(element);
}
}
Read side:
for (int i = 0; i < count; i++)
{
bool hasValue = _reader.ReadBoolean();
if (hasValue)
{
array[i] = _reader.ReadString();
}
// else: array[i] remains null (default)
}
Scope of Changes
Analysis confirms: Task outputs are limited to scalar types and arrays (string[], value type arrays, ITaskItem[]) per TaskParameterTypeVerifier.
The List<string> and HashSet<string> direct translation methods in BinaryTranslator are not actually used anywhere in the codebase. All List<string> usages go through the generic Translate<T>(ref List<T>, ObjectTranslator<T>) overload, which calls Translate(ref string) per element - and that method already handles null via TranslateNullable().
Therefore, we only need to fix string[] - the only collection type affected by this bug.
Files to modify:
-
src/Framework/BinaryTranslator.cs - Fix serialization for:
BinaryWriteTranslator.Translate(ref string[] array)
BinaryReadTranslator.Translate(ref string[] array)
-
src/Shared/UnitTests/TaskParameter_Tests.cs - Add test for null elements in string arrays
-
src/Build.UnitTests/BackEnd/TaskHostTaskComplete_Tests.cs - Add end-to-end test for the TaskHost scenario
Work Plan
Notes and Considerations
Binary Format Compatibility
This change modifies the wire format of serialized string arrays. However:
- TaskHost communication is within a single MSBuild process family (parent + task host children)
- All processes are from the same MSBuild version
- No cross-version compatibility concerns for this specific serialization path
Performance
- Adds 1 byte per string element (boolean marker)
- Minimal impact on serialization performance
- Hot path performance is maintained
Why only string[] needs fixing
Task outputs are restricted to scalar types and arrays per TaskParameterTypeVerifier. The List<string> and HashSet<string> translation methods are only used for internal MSBuild serialization (e.g., SdkResult), which don't contain null elements. Only string[] task outputs can have nulls from user tasks.
Alternative Approaches Considered
- Filter nulls before serialization: Would change semantics; task might depend on null positions
- Use different encoding for null (e.g., empty string marker): Ambiguous with actual empty strings
- Throw earlier with descriptive error: Doesn't fix the underlying limitation
The boolean marker approach is the cleanest solution that preserves null element semantics.
Summary
MSBuild's
-mtmode crashes when a task returns aString[]containing null elements. TheBinaryTranslatorserialization code doesn't handle null array elements.Stack Trace
Reproduction
Any task returning
String[]with null elements crashes in-mtmode:Risk Assessment
High risk for custom tasks - Common patterns trigger this:
items.Select(x => x?.Value).ToArray()A
String[]with null elements is valid .NET; MSBuild should serialize it correctly.Fix plan proposal: TaskHost should correctly serialize
string[]containing null elementsProblem Statement
MSBuild's
-mtmode TaskHost crashes when a task returns aString[]containing null elements. TheBinaryTranslatorserialization code callsBinaryWriter.Write(null)which throwsArgumentNullException.Stack Trace:
Real-World Impact: The SDK's
GenerateGlobalUsingstask and similar tasks can trigger this bug. When building aspnetcore with-mtmode, TaskHost worker processes crash.Analysis Confirmed
Why TaskHost is affected but normal worker nodes are not
TaskHost path (VULNERABLE): Task outputs are serialized via
TaskHostTaskComplete→TaskParameter→BinaryTranslator.Translate(ref string[] array). This directly callsBinaryWriter.Write(array[i])without null checking.Normal worker path (SAFE): Task outputs flow through in-process
IBuildEnginecallbacks that filter/convert values before any binary serialization occurs. The raw task output objects with null array elements never reach the serialization layer.Root Cause
In
BinaryTranslator.cs, theBinaryWriteTranslator.Translate(ref string[] array)method (lines 1159-1173) iterates over the array and calls_writer.Write(array[i])without checking for null elements:Same issue affects:
List<string>(lines 1189-1192)HashSet<string>(lines 1206-1208)Proposed Solution
Add null element handling by writing a boolean marker before each string element:
Write side:
Read side:
Scope of Changes
Analysis confirms: Task outputs are limited to scalar types and arrays (
string[], value type arrays,ITaskItem[]) perTaskParameterTypeVerifier.The
List<string>andHashSet<string>direct translation methods inBinaryTranslatorare not actually used anywhere in the codebase. AllList<string>usages go through the genericTranslate<T>(ref List<T>, ObjectTranslator<T>)overload, which callsTranslate(ref string)per element - and that method already handles null viaTranslateNullable().Therefore, we only need to fix
string[]- the only collection type affected by this bug.Files to modify:
src/Framework/BinaryTranslator.cs- Fix serialization for:BinaryWriteTranslator.Translate(ref string[] array)BinaryReadTranslator.Translate(ref string[] array)src/Shared/UnitTests/TaskParameter_Tests.cs- Add test for null elements in string arrayssrc/Build.UnitTests/BackEnd/TaskHostTaskComplete_Tests.cs- Add end-to-end test for the TaskHost scenarioWork Plan
1. Add unit test for string[] with null elements in TaskParameter_Tests.cs
TaskParametercorrectly serializes and deserializes astring[]with null elementsPrimitiveArrayParametertest2. Add end-to-end test in TaskHostTaskComplete_Tests.cs
TaskHostTaskCompletewith string array output containing nulls serializes correctly3. Fix BinaryWriteTranslator.Translate(ref string[] array)
4. Fix BinaryReadTranslator.Translate(ref string[] array)
5. Run unit tests to verify the fix
6. Run broader test suite to ensure no regressions
Notes and Considerations
Binary Format Compatibility
This change modifies the wire format of serialized string arrays. However:
Performance
Why only string[] needs fixing
Task outputs are restricted to scalar types and arrays per
TaskParameterTypeVerifier. TheList<string>andHashSet<string>translation methods are only used for internal MSBuild serialization (e.g.,SdkResult), which don't contain null elements. Onlystring[]task outputs can have nulls from user tasks.Alternative Approaches Considered
The boolean marker approach is the cleanest solution that preserves null element semantics.