From 8a545ca34248d1282f4aef28777f15c8cdf95d4a Mon Sep 17 00:00:00 2001 From: Azat Mukhametshin Date: Wed, 9 Sep 2026 15:07:57 +0000 Subject: [PATCH 1/2] Fix dotnet host resolution under proot (ARM64/Termux) ProcessHelper.GetCurrentProcessFileName used Process.MainModule.FileName to identify the running executable. Under proot the value can be the injected proot loader instead of the running dotnet host, so DotnetTestHostManager and DotnetHostHelper stop recognizing the current process as dotnet, fall back to searching DOTNET_ROOT* and the well known installation folders, and finally fail with: Could not find 'dotnet' host for the '' architecture. .NET builds MainModule from /proc//maps. proot translates readlink("/proc/self/exe") to the guest path, but it cannot rewrite the contents of /proc//maps, which keep the host path. When the two differ, ProcessManager.GetModules cannot match the executable and leaves the lowest mapped module first. On ARM64 proot maps its loader below the executable (LOADER_ADDRESS 0x2000000000 < EXEC_PIC_ADDRESS 0x3000000000, see proot src/arch.h), so that first module is the loader. On x64 the order is reversed, which is why this only reproduces on ARM64. Prefer Environment.ProcessPath on .NET. It does not depend on the /proc maps ordering and proot reports it correctly. The change is guarded by #if NET so .NET Framework keeps the existing MainModule behavior, and it falls back to MainModule when ProcessPath is null. Verified with real proot 5.4.1 by reproducing both required conditions on x64 (guest/host path mismatch plus a loader relocated below the executable): baseline aborts with the error above, the fixed build runs the tests. No behavior change outside proot. Fixes #16446 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../common/System/ProcessHelper.cs | 14 +++++++++ .../ProcessHelperTests.cs | 29 +++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.TestPlatform.PlatformAbstractions/common/System/ProcessHelper.cs b/src/Microsoft.TestPlatform.PlatformAbstractions/common/System/ProcessHelper.cs index cb40d81de8..9667792857 100644 --- a/src/Microsoft.TestPlatform.PlatformAbstractions/common/System/ProcessHelper.cs +++ b/src/Microsoft.TestPlatform.PlatformAbstractions/common/System/ProcessHelper.cs @@ -245,6 +245,20 @@ internal static void WaitForErrorStreamToDrain(ManualResetEventSlim? errorStream /// public string? GetCurrentProcessFileName() { +#if NET + // Environment.ProcessPath is more reliable than MainModule under sandboxes such as + // proot, which is commonly used to run Linux distributions on Android (e.g. Termux). + // proot starts the real executable through an injected loader, and it cannot rewrite + // the contents of /proc/self/maps that MainModule is built from. When the loader is + // mapped below the executable (as it is on ARM64) MainModule reports the loader + // instead of the running dotnet host, which breaks test host resolution. + // See https://github.com/microsoft/vstest/issues/16446. + if (Environment.ProcessPath is { } processPath) + { + return processPath; + } +#endif + return _currentProcess.MainModule?.FileName; } diff --git a/test/vstest.console.UnitTests/ProcessHelperTests.cs b/test/vstest.console.UnitTests/ProcessHelperTests.cs index d9a00cff82..a2d0b12c8f 100644 --- a/test/vstest.console.UnitTests/ProcessHelperTests.cs +++ b/test/vstest.console.UnitTests/ProcessHelperTests.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +#if NET +using System; +#endif using System.Diagnostics; using System.Threading; @@ -11,17 +14,39 @@ namespace Microsoft.VisualStudio.TestPlatform.CommandLine.UnitTests; /// -/// Tests for , the bounded wait that lets the process +/// Tests for . +/// +/// +/// is the bounded wait that lets the process /// exit callback observe the complete standard error output of a crashed test host. Without it, the exit /// callback could read the asynchronously-collected stderr before all ErrorDataReceived callbacks had run, /// dropping a crash callstack such as "Stack overflow." (the cause of the flaky /// RunTestsShouldThrowOnStackOverflowException test). -/// +/// [TestClass] public class ProcessHelperTests { private const int BudgetMs = 500; + [TestMethod] + public void GetCurrentProcessFileNameShouldReturnThePathOfTheRunningExecutable() + { + var processHelper = new ProcessHelper(); + + string? currentProcessFileName = processHelper.GetCurrentProcessFileName(); + + Assert.IsNotNull(currentProcessFileName, "The running executable must always be identifiable."); +#if NET + // On .NET we prefer Environment.ProcessPath, because MainModule can report an injected loader + // rather than the running executable under sandboxes such as proot. See issue #16446. + Assert.AreEqual(Environment.ProcessPath, currentProcessFileName); +#else + // .NET Framework has no Environment.ProcessPath, so the MainModule behavior must be preserved. + using var currentProcess = Process.GetCurrentProcess(); + Assert.AreEqual(currentProcess.MainModule?.FileName, currentProcessFileName); +#endif + } + [TestMethod] public void WaitForErrorStreamToDrainShouldReturnOnceTheErrorStreamCloses() { From 40107d8e84789d13610b6fa074117ddd6be4a18b Mon Sep 17 00:00:00 2001 From: Azat Mukhametshin Date: Wed, 9 Sep 2026 15:20:43 +0000 Subject: [PATCH 2/2] Mirror the ProcessPath fallback in the test Environment.ProcessPath is nullable, and GetCurrentProcessFileName falls back to MainModule when it is null. The test asserted against ProcessPath directly, so it would have failed in a host where ProcessPath is null even though the production fallback behaved correctly. Compute the expected value the same way the production code does instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/vstest.console.UnitTests/ProcessHelperTests.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/vstest.console.UnitTests/ProcessHelperTests.cs b/test/vstest.console.UnitTests/ProcessHelperTests.cs index a2d0b12c8f..c1e2ea66f3 100644 --- a/test/vstest.console.UnitTests/ProcessHelperTests.cs +++ b/test/vstest.console.UnitTests/ProcessHelperTests.cs @@ -35,16 +35,21 @@ public void GetCurrentProcessFileNameShouldReturnThePathOfTheRunningExecutable() string? currentProcessFileName = processHelper.GetCurrentProcessFileName(); - Assert.IsNotNull(currentProcessFileName, "The running executable must always be identifiable."); + using var currentProcess = Process.GetCurrentProcess(); + string? mainModuleFileName = currentProcess.MainModule?.FileName; + #if NET // On .NET we prefer Environment.ProcessPath, because MainModule can report an injected loader // rather than the running executable under sandboxes such as proot. See issue #16446. - Assert.AreEqual(Environment.ProcessPath, currentProcessFileName); + // ProcessPath is nullable, so mirror the fallback the production code performs instead of + // assuming it is always set. + string? expectedFileName = Environment.ProcessPath ?? mainModuleFileName; #else // .NET Framework has no Environment.ProcessPath, so the MainModule behavior must be preserved. - using var currentProcess = Process.GetCurrentProcess(); - Assert.AreEqual(currentProcess.MainModule?.FileName, currentProcessFileName); + string? expectedFileName = mainModuleFileName; #endif + + Assert.AreEqual(expectedFileName, currentProcessFileName); } [TestMethod]