Skip to content

Taskhost should correctly serialize string[] containing null elements #13174

@JanProvaznik

Description

@JanProvaznik

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

  1. TaskHost path (VULNERABLE): Task outputs are serialized via TaskHostTaskCompleteTaskParameterBinaryTranslator.Translate(ref string[] array). This directly calls BinaryWriter.Write(array[i]) without null checking.

  2. 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:

  1. src/Framework/BinaryTranslator.cs - Fix serialization for:

    • BinaryWriteTranslator.Translate(ref string[] array)
    • BinaryReadTranslator.Translate(ref string[] array)
  2. src/Shared/UnitTests/TaskParameter_Tests.cs - Add test for null elements in string arrays

  3. src/Build.UnitTests/BackEnd/TaskHostTaskComplete_Tests.cs - Add end-to-end test for the TaskHost scenario

Work Plan

  • 1. Add unit test for string[] with null elements in TaskParameter_Tests.cs

    • Test that TaskParameter correctly serializes and deserializes a string[] with null elements
    • Similar pattern to existing PrimitiveArrayParameter test
  • 2. Add end-to-end test in TaskHostTaskComplete_Tests.cs

    • Test that TaskHostTaskComplete with string array output containing nulls serializes correctly
  • 3. Fix BinaryWriteTranslator.Translate(ref string[] array)

    • Add null check before writing each element
    • Write boolean marker to indicate presence of value
  • 4. Fix BinaryReadTranslator.Translate(ref string[] array)

    • Read boolean marker before reading each element
    • Only read string if marker indicates non-null
  • 5. Run unit tests to verify the fix

    • Run TaskParameter_Tests
    • Run TaskHostTaskComplete_Tests
  • 6. Run broader test suite to ensure no regressions

    • Build and run tests for the affected projects

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

  1. Filter nulls before serialization: Would change semantics; task might depend on null positions
  2. Use different encoding for null (e.g., empty string marker): Ambiguous with actual empty strings
  3. Throw earlier with descriptive error: Doesn't fix the underlying limitation

The boolean marker approach is the cleanest solution that preserves null element semantics.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions