Visitar URL original
Keep Unix named mutex storage mapped until a native owner thread exits by jkoritzinsky · Pull Request #137223 · dotnet/runtime · GitHub
Skip to content

Keep Unix named mutex storage mapped until a native owner thread exits - #137223

Open
jkoritzinsky wants to merge 5 commits into
dev/jkoritzinsky/redesigned-potatofrom
dev/jkoritzinsky/named-mutex-native-owner-exit
Open

jkoritzinsky wants to merge 5 commits into
dev/jkoritzinsky/redesigned-potatofrom
dev/jkoritzinsky/named-mutex-native-owner-exit

Conversation

@jkoritzinsky

@jkoritzinsky jkoritzinsky commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

Stacked on #136737. This targets that PR's branch (dev/jkoritzinsky/redesigned-potato, head 472d76e0f30) 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.Close calls pthread_mutex_destroy and munmap while the dying OS thread still has the mutex on its kernel robust futex list. The kernel's exit-time exit_robust_list walk 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 getting AbandonedMutexException. There is no crash. With a single owned mutex there is no visible effect.

The PR's AbandonLockCore change skips the foreign unlock, but the reference is still dropped and the mapping still unmapped, so this failure remains.

Fix

  • Record Thread.CurrentOSThreadId when ownership is recorded.
  • In the pthread AbandonLockCore override, when cleanup runs on a thread other than the owner, determine the owner OS thread's state (Running, Exited or Unknown):
    • Exited: nothing changes; the normal close, destroy and unmap proceed.
    • Running: take an extra mapping reference and register a Gen2GcCallback that keeps returning true until 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.
  • The state comes from a new SystemNative_TgKill(pid, tid, 0) helper (syscall(SYS_tgkill) on Linux, ENOSYS elsewhere, including a wasi stub). ESRCH means exited (the kernel unhashes a thread only after its robust list walk); success or any other error such as EPERM means running.
  • If tgkill fails with ENOSYS (unsupported platform or a filtered syscall), fall back to /proc/self/task/<tid>: present means running, absent means exited. If /proc/self/task is not available either, the state is Unknown.
  • Owner-thread abandonment (managed Thread.Start threads) 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.

  • Standalone native harness mirroring pal_crossprocessmutex.c: with a non-owner unlock, destroy and munmap of the last-locked mutex, the earlier mutex stays locked after the owner thread exits (EBUSY/ETIMEDOUT instead of EOWNERDEAD); the control without the unmap recovers (EOWNERDEAD).
  • Managed end-to-end repro (native thread acquires two named mutexes, public handles disposed, OS thread held alive after runtime detach with a pthread-key destructor, finalizers driven with GC.Collect(), then the other mutex waited on):
    • Baseline and Fix Unix named mutex ownership recovery races #136737 head (checked on an earlier build of the same file contents): the other mutex is stranded (WaitOne(3000) returns false) in 10/10 runs.
    • This branch with 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.
    • This branch with tgkill forced to fail with ENOSYS (an LD_PRELOAD shim, 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's NamedMutex_ReacquireBeforeAbandonedOwnerCleanup (3 rows) and NamedMutex_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 touches NamedMutex.Unix.cs).

Limitations

  • No unit test is included. The failing window needs the OS thread held alive after the runtime detaches it, which needs a native shim and cannot be arranged from managed test code; the repro above is the end-to-end check.
  • Only Linux x64 was run. The wasi/browser System.Native builds and non-Linux backends were not built or run, and the Unknown state (no tgkill and no /proc) was not exercised.
  • The branch history has several commits because the push was done in pieces; the final tree is the single intended change.

Note

This pull request was generated with AI assistance (GitHub Copilot) and should be reviewed accordingly.

jkoritzinsky and others added 3 commits November 9, 2026 14:30
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

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
See info in area-owners.md if you want to be subscribed.

…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.
if (state != OSThreadState.Exited)
{
ProcessDataHeader.IncrementRefCount();
if (state == OSThreadState.Running)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants