Fix dotnet host resolution under proot (ARM64/Termux) - #16462
Fix dotnet host resolution under proot (ARM64/Termux)#16462Azat Mukhametshin (azat-msft) wants to merge 2 commits into
Conversation
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 '<arch>' architecture.
.NET builds MainModule from /proc/<pid>/maps. proot translates
readlink("/proc/self/exe") to the guest path, but it cannot rewrite the
contents of /proc/<pid>/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 microsoft#16446
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new unit test currently assumes Environment.ProcessPath is always non-null on .NET, despite the production code explicitly supporting a fallback path.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR addresses dotnet host resolution failures when running dotnet test inside proot (notably on ARM64/Termux) by making ProcessHelper.GetCurrentProcessFileName() prefer Environment.ProcessPath on .NET, avoiding incorrect Process.MainModule.FileName values when proot injects a loader.
Changes:
- Update
ProcessHelper.GetCurrentProcessFileName()to returnEnvironment.ProcessPathon .NET (with a fallback toMainModulewhen unavailable). - Add a unit test validating
GetCurrentProcessFileName()behavior across TFMs.
File summaries
| File | Description |
|---|---|
| src/Microsoft.TestPlatform.PlatformAbstractions/common/System/ProcessHelper.cs | Prefer Environment.ProcessPath to correctly identify the running executable under proot/sandboxed environments. |
| test/vstest.console.UnitTests/ProcessHelperTests.cs | Add a regression test for GetCurrentProcessFileName() and update class documentation. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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>
There was a problem hiding this comment.
🟢 Approval recommended
The change is narrowly scoped, matches the documented root cause in proot, and is covered by a new cross-TFM unit test mirroring the production fallback behavior.
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Lite
Fixes #16446
Problem
Running
dotnet testinside proot (commonly used to run a Linux distribution on Android via Termux) fails with:ProcessHelper.GetCurrentProcessFileName()returnsProcess.MainModule?.FileName. Under proot this can be proot's injected loader rather than the runningdotnethost.DotnetTestHostManagerandDotnetHostHelperthen stop recognizing the current process asdotnet, fall back to searchingDOTNET_ROOT*and the well-known install locations, and ultimately fail.The reporter observed all three values in one process:
Environment.ProcessPath/usr/share/dotnet/dotnet✅/proc/self/exe/usr/share/dotnet/dotnet✅Process.MainModule.FileName.../libexec/proot/loader❌Root cause
.NET builds
MainModulefrom/proc/<pid>/maps.ProcessManager.GetModules(ProcessManager.Linux.cs) parses that file and then tries to hoist the main executable to index 0 by matchingmodule.FileName == Process.GetExePath(pid).MainModuleis simplyModules[0].Two conditions must hold at the same time for the wrong value to be returned:
Guest/host path mismatch. proot translates
readlink("/proc/self/exe")to the guest path, but it cannot rewrite the contents of/proc/<pid>/maps, which keep the host path. When the two strings differ the hoist silently fails, soModules[0]is whatever is mapped lowest.The loader is mapped below the executable. From proot's
src/arch.h:LOADER_ADDRESSEXEC_PIC_ADDRESS0x6000000000000x5000000000000x20000000000x30000000000x100000000x0f0000000xa00000000x0f000000ARM64 is the only architecture where proot maps its loader below the executable, which is why this reproduces on ARM64 (Android/Termux) and not on x64.
Note that setting
DOTNET_ROOTdoes not work around it:DotnetHostHelper.IsValidArchitectureMuxeronly inspects Windows and macOS executable formats, so a configured Linuxdotnetis rejected as well. That gap is left as-is here and is worth a separate look.Fix
Prefer
Environment.ProcessPathon .NET. It does not depend on/procmaps ordering, and proot reports it correctly. Guarded with#if NETso .NET Framework keeps the existingMainModulebehavior, with a fallback toMainModuleifProcessPathis evernull.Validation
Reproduced with real proot 5.4.1 on x64 Linux (WSL2) by forcing both required conditions — a guest/host path mismatch, plus the genuine proot loader relocated below the executable via
PROOT_LOADERto mirror the ARM64 layout. No process information was mocked or simulated.Probe under those conditions, matching the report exactly:
End-to-end
dotnet test, where the only difference is this change (built assembly injected into a copy of an SDK):Could not find 'dotnet' host for the 'X64' architecture./ Test Run AbortedPassed! - Failed: 0, Passed: 1The fixed build was also confirmed to work outside proot and inside plain proot, so there is no behavior change in normal environments.
Tests
Added
GetCurrentProcessFileNameShouldReturnThePathOfTheRunningExecutabletoProcessHelperTests, using conditional compilation so it compiles and passes on both target frameworks (Environment.ProcessPathdoes not exist on .NET Framework, which would otherwise break thenet481build with CS0117).Local Release build: 0 warnings, 0 errors across all target frameworks.
Unit test suites touching this code path, compared against the same build with the change stashed (the pre-existing failures are Windows-only tests that do not run correctly on Linux):
vstest.console.UnitTestsMicrosoft.TestPlatform.CoreUtilities.UnitTestsMicrosoft.TestPlatform.CrossPlatEngine.UnitTestsRemaining uncertainty
The reproduction was done on x64 by emulating ARM64's loader placement. I did not have an ARM64 Android/Termux device available, so a confirmation on the reporter's original setup would be welcome.