diff --git a/src/Build.UnitTests/BackEnd/StringArrayWithNullsTask.cs b/src/Build.UnitTests/BackEnd/StringArrayWithNullsTask.cs
new file mode 100644
index 00000000000..657ef618657
--- /dev/null
+++ b/src/Build.UnitTests/BackEnd/StringArrayWithNullsTask.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+#nullable disable
+
+namespace Microsoft.Build.UnitTests.BackEnd
+{
+ ///
+ /// A task that returns a string array containing null elements.
+ /// Used to test the fix for https://github.com/dotnet/msbuild/issues/13174
+ ///
+ public class StringArrayWithNullsTask : Task
+ {
+ [Output]
+ public string[] OutputArray { get; set; }
+
+ [Output]
+ public int Pid { get; set; }
+
+ public override bool Execute()
+ {
+ // Return an array with nulls - this pattern occurs in real tasks like GenerateGlobalUsings
+ OutputArray = new string[] { "first", null, "third", null, "fifth" };
+ Pid = Process.GetCurrentProcess().Id;
+ return true;
+ }
+ }
+}
diff --git a/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs
index 9b80c023c94..22178bbe618 100644
--- a/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs
+++ b/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs
@@ -363,5 +363,45 @@ public void VariousParameterTypesCanBeTransmittedToAndReceivedFromTaskHost()
projectInstance.GetPropertyValue("CustomStructOutput").ShouldBe(TaskBuilderTestTask.s_customStruct.ToString(CultureInfo.InvariantCulture));
projectInstance.GetPropertyValue("EnumOutput").ShouldBe(TargetBuiltReason.BeforeTargets.ToString());
}
+
+ ///
+ /// Verifies that a task returning a string[] with null elements does not crash
+ /// when executed via TaskHostFactory. This is a regression test for
+ /// https://github.com/dotnet/msbuild/issues/13174
+ ///
+ [Fact]
+ public void StringArrayWithNullsDoesNotCrashTaskHost()
+ {
+ using TestEnvironment env = TestEnvironment.Create();
+
+ string projectContents = $@"
+
+
+
+ <{typeof(StringArrayWithNullsTask).Name}>
+
+
+ {typeof(StringArrayWithNullsTask).Name}>
+
+";
+
+ TransientTestFile project = env.CreateFile("testProject.csproj", projectContents);
+ ProjectInstance projectInstance = new(project.Path);
+ BuildManager buildManager = BuildManager.DefaultBuildManager;
+ BuildResult buildResult = buildManager.Build(new BuildParameters(), new BuildRequestData(projectInstance, targetsToBuild: new[] { "TestTarget" }));
+
+ // The build should succeed - nulls should be filtered, not cause a crash
+ buildResult.OverallResult.ShouldBe(BuildResultCode.Success);
+
+ // Verify task ran out-of-process (TaskHostFactory should force this)
+ string taskPidStr = projectInstance.GetPropertyValue("TaskPid");
+ taskPidStr.ShouldNotBeNullOrEmpty();
+ int.TryParse(taskPidStr, out int taskPid).ShouldBeTrue();
+ Process.GetCurrentProcess().Id.ShouldNotBe(taskPid, "Task should have run in a separate TaskHost process");
+
+ // Verify output items - nulls should be filtered out, leaving 3 items
+ var outputItems = projectInstance.GetItems("OutputItems");
+ outputItems.Count.ShouldBe(3, "Null elements should be filtered from the string array");
+ }
}
}
diff --git a/src/MSBuild/OutOfProcTaskAppDomainWrapperBase.cs b/src/MSBuild/OutOfProcTaskAppDomainWrapperBase.cs
index ae77fb43e03..0ad4fc8fe6b 100644
--- a/src/MSBuild/OutOfProcTaskAppDomainWrapperBase.cs
+++ b/src/MSBuild/OutOfProcTaskAppDomainWrapperBase.cs
@@ -419,7 +419,16 @@ private OutOfProcTaskHostTaskResult InstantiateAndExecuteTask(
{
try
{
- finalParameterValues[value.Name] = value.GetValue(wrappedTask, null);
+ object outputValue = value.GetValue(wrappedTask, null);
+
+ // Filter null elements from string[] outputs to avoid crash.
+ // See https://github.com/dotnet/msbuild/issues/13174
+ if (outputValue is string[] stringArray)
+ {
+ outputValue = FilterNullsFromStringArray(stringArray, value.Name);
+ }
+
+ finalParameterValues[value.Name] = outputValue;
}
catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
{
@@ -451,5 +460,46 @@ private void LogErrorDelegate(string taskLocation, int taskLine, int taskColumn,
null,
taskName));
}
+
+ ///
+ /// Filters null elements from a string[] task output.
+ /// See https://github.com/dotnet/msbuild/issues/13174
+ ///
+ private string[] FilterNullsFromStringArray(string[] stringArray, string parameterName)
+ {
+ // Count nulls
+ int nullCount = 0;
+ foreach (string s in stringArray)
+ {
+ if (s == null)
+ {
+ nullCount++;
+ }
+ }
+
+ if (nullCount == 0)
+ {
+ return stringArray;
+ }
+
+ // Filter nulls and log
+ string[] filtered = new string[stringArray.Length - nullCount];
+ int j = 0;
+ foreach (string s in stringArray)
+ {
+ if (s != null)
+ {
+ filtered[j++] = s;
+ }
+ }
+
+ buildEngine.LogMessageEvent(new BuildMessageEventArgs(
+ message: ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TaskHostAcquired_NullsFiltered", parameterName, nullCount),
+ helpKeyword: null,
+ senderName: taskName,
+ importance: MessageImportance.Normal));
+
+ return filtered;
+ }
}
}
diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx
index 6e75cc0db7e..797d284b563 100644
--- a/src/MSBuild/Resources/Strings.resx
+++ b/src/MSBuild/Resources/Strings.resx
@@ -1719,6 +1719,13 @@
+
+
+ Task output parameter "{0}" contained {1} null element(s) which have been filtered out.
+ LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered.
+
+
+