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..c1e2ea66f3 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,44 @@
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();
+
+ 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.
+ // 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.
+ string? expectedFileName = mainModuleFileName;
+#endif
+
+ Assert.AreEqual(expectedFileName, currentProcessFileName);
+ }
+
[TestMethod]
public void WaitForErrorStreamToDrainShouldReturnOnceTheErrorStreamCloses()
{