Repository navigation
Keep Unix named mutex storage mapped until a native owner thread exits - #137223
Open
jkoritzinsky wants to merge 5 commits into
Open
jkoritzinsky wants to merge 5 commits into
jkoritzinsky wants to merge 5 commits into
Conversation
When a native (non-.NET) thread owns a pthread-backed named mutex and exits, the runtime detaches the thread before the OS thread has actually exited, and the managed ownership cleanup is deferred to the finalizer thread. If that cleanup dropped the last reference to the shared memory, it unmapped the memory that the exiting OS thread still has on its kernel robust futex list. The kernel's exit-time robust list walk then faults and silently stops, so every mutex that the thread locked earlier is never marked owner-dead and stays locked forever. Record the owner's OS thread id when ownership is recorded. When deferred cleanup runs on a thread other than the owner and the OS thread may still be alive, hold an extra mapping reference with a finalizable watcher that re-registers for finalization until the thread is gone, then releases it. Liveness is checked with a new SystemNative_TgKill helper; only ESRCH counts as exited, and platforms that cannot detect exit keep the reference until process exit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When deferred cleanup abandons a pthread-backed named mutex on a thread other than its owner, the owner's OS thread may not have exited yet. Record the owner's OS thread id, and if it may still be alive, hold an extra mapping reference with a finalizable watcher that re-registers until SystemNative_TgKill reports the thread gone, then releases it. This keeps the mapping valid for the kernel's exit-time robust futex list walk.
|
Azure Pipelines: Successfully started running 3 pipeline(s). 12 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
|
Tagging subscribers to this area: @JulieLeeMSFT, @VSadov |
…e owner exits Replace the one-off finalizable watcher with the existing Gen2GcCallback helper, which already implements re-registering for finalization until the callback reports that it is done.
When SystemNative_TgKill fails with ENOSYS (a platform without tgkill, or a filtered syscall), check /proc/self/task/<tid> instead. If neither mechanism can tell, keep the mapping reference until process exit.
jkotas
reviewed
Nov 10, 2026
| if (state != OSThreadState.Exited) | ||
| { | ||
| ProcessDataHeader.IncrementRefCount(); | ||
| if (state == OSThreadState.Running) |
Member
There was a problem hiding this comment.
Instead of this retry loop, would be possible/better to maintain a list of the mutexes owned by a thread in C/C++ and mark those mutexes as invalid in TLS detach callback?
maraf
pushed a commit
that referenced
this pull request
Nov 11, 2026
Removes `extraBuildArgs.Add("-v:diag");` from
`ReadyToRunTests.PublishCrossgen2ArgumentsReflectWasmDebugInfoProperties`.
The theory matrix and assertions are unchanged.
## Why
The Helix work item `WBT-NoWorkload-CLR-Wasm.Build.Tests` on
`windows.server2025.amd64.open` is dead-lettering on main and on PRs.
Examples: main build 1672437, and #136772 builds 1670137 and 1672543.
Each one fails with exit -1 after 0 s and no console log.
- Helix heartbeats show each attempt runs for exactly 90 min, which is
the `_workItemTimeout` of 01:30:00. The machine then reboots and the
item retries on another machine. After 3 attempts it dead-letters.
- The median Windows duration for this work item went from about 55 min
(Nov 3–4) to about 91 min (Nov 5 onward). That change lines up with
#136506 (b0643c8, merged Nov 5). It added 12
`[TestCategory("no-workload")]` cases of this test, and each one
publishes with `-v:diag`.
- In a passing slow run (Helix job
`a5e1304b-0299-470a-a3ce-9a2289a5b925`, from #137223), `testResults.xml`
was **1.47 GB**. In a run from before #136506 (job
`e197dfa2-77e6-46f3-8c6c-b0c6903841b1`) it was **504 KB**. Each of these
cases captured about 120 MB (~1M lines) of diag MSBuild output and took
about 121 s.
The test doesn't need diagnostic verbosity.
`GetCrossgen2ExtraCommandLineArgs(binlogPath)` reads the
`Crossgen2ExtraCommandLineArgs` parameter of the `RunReadyToRunCompiler`
task from the binlog, and the binary logger records task parameters at
any console verbosity.
## Validation
- `./dotnet.sh build
src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj -c Release
-p:TargetOS=browser -p:TargetArchitecture=wasm -p:RuntimeFlavor=CoreCLR
-p:InstallWorkloadForTesting=false`: **passed** (0 warnings, 0 errors).
- Without `-p:InstallWorkloadForTesting=false`, the test assembly still
compiled. The build then failed in the testing-SDK provisioning step
(`workloads-browser.targets(122,5)`), because this worktree has no
locally built runtime packs.
- I did **not** run the test locally, since that needs a full
browser-wasm CoreCLR build. CI coverage of the Windows
`WBT-NoWorkload-CLR` work item is the real check.
> [!NOTE]
> This PR description was generated with GitHub Copilot.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #136737. This targets that PR's branch (
dev/jkoritzinsky/redesigned-potato, head472d76e0f30) and fixes a failure the PR does not cover.Problem
When a native (non-.NET) thread owns a pthread-backed named mutex and exits, the runtime detaches the thread while its OS thread is still alive, and the managed ownership cleanup is deferred to the finalizer thread. If that cleanup drops the last reference to the shared memory,
SharedMemoryProcessDataHeader.Closecallspthread_mutex_destroyandmunmapwhile the dying OS thread still has the mutex on its kernel robust futex list. The kernel's exit-timeexit_robust_listwalk then faults reading the unmapped node and silently stops. Every mutex the thread locked earlier is never marked owner-dead and stays locked forever; other waiters time out instead of gettingAbandonedMutexException. There is no crash. With a single owned mutex there is no visible effect.The PR's
AbandonLockCorechange skips the foreign unlock, but the reference is still dropped and the mapping still unmapped, so this failure remains.Fix
Thread.CurrentOSThreadIdwhen ownership is recorded.AbandonLockCoreoverride, when cleanup runs on a thread other than the owner, determine the owner OS thread's state (Running,ExitedorUnknown):Exited: nothing changes; the normal close, destroy and unmap proceed.Running: take an extra mapping reference and register aGen2GcCallbackthat keeps returningtrueuntil the thread has exited, then releases the reference under the creation/deletion lock, which runs the normal close and unlink.Unknown: keep the extra reference until process exit, with no retries.SystemNative_TgKill(pid, tid, 0)helper (syscall(SYS_tgkill)on Linux,ENOSYSelsewhere, including a wasi stub).ESRCHmeans exited (the kernel unhashes a thread only after its robust list walk); success or any other error such asEPERMmeans running.tgkillfails withENOSYS(unsupported platform or a filtered syscall), fall back to/proc/self/task/<tid>: present means running, absent means exited. If/proc/self/taskis not available either, the state isUnknown.Thread.Startthreads) is unchanged.Cost: the retry runs after gen 2 GCs, so a quiet process may hold the mapping until one happens; one mapping per event.
Validation
Linux x64, glibc 2.43, kernel 6.18 (WSL2). Release build (
./build.sh clr+libs -rc release -lc release) of this branch, i.e. on #136737's base.pal_crossprocessmutex.c: with a non-ownerunlock,destroyandmunmapof the last-locked mutex, the earlier mutex stays locked after the owner thread exits (EBUSY/ETIMEDOUTinstead ofEOWNERDEAD); the control without the unmap recovers (EOWNERDEAD).GC.Collect(), then the other mutex waited on):WaitOne(3000)returns false) in 10/10 runs.tgkill: 10/10 runs recover it as abandoned. The mapping is present while the owner OS thread is alive and released after it exits. The control with a live alias keeps the mapping in 10/10 runs.tgkillforced to fail withENOSYS(anLD_PRELOADshim, confirmed to intercept the calls): the procfs fallback recovers it in 5/5 runs, with the mapping released after the owner exits.dotnet build /t:test src/libraries/System.Threading/tests/System.Threading.Tests.csproj -c Release: Total 770, Failed 0, Skipped 17, including Fix Unix named mutex ownership recovery races #136737'sNamedMutex_ReacquireBeforeAbandonedOwnerCleanup(3 rows) andNamedMutex_DeferredAbandonment_DoesNotModifyRecoveredMutex(4 rows).dotnet build /t:test src/libraries/System.Threading.Thread/tests/System.Threading.Thread.Tests.csproj -c Release: Total 60, Failed 0, Skipped 9 (run before the procfs fallback commit, which only touchesNamedMutex.Unix.cs).Limitations
Unknownstate (notgkilland no/proc) was not exercised.Note
This pull request was generated with AI assistance (GitHub Copilot) and should be reviewed accordingly.