Fix COW race condition in mem_ptr under concurrent write faults - #2706
Fix COW race condition in mem_ptr under concurrent write faults#2706evnchn wants to merge 1 commit into
Conversation
…load The data pointer was computed from the page table entry while holding a read lock, then used after releasing the read lock and re-acquiring a write lock. Between the lock release and re-acquire, another thread could free or remap the page, leaving data as a dangling pointer. The subsequent memcpy would then crash in _platform_memmove. Fix by re-fetching the page table entry after acquiring the write lock. If another thread already handled the COW, discard the allocated copy. Reproduces reliably with Node.js worker threads (e.g. Claude Code TUI spawning 10+ threads), causing iSH to crash to the iOS home screen. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
I tried to produce an empirical before/after for this one and could not trigger the race, so treating that as a limitation of my harness rather than evidence against the patch — writing up both so the attempt is not repeated blindly. What I ran: a Two caveats that make this weak evidence:
Reading the code, the hazard looks real regardless. The original computes void *data = (char *) entry->data->data + entry->offset;before dropping the read lock, then uses that pointer and One data point that may be worth something: our fork hit this area too and ended up eliminating the same window by a different route — taking the write lock up front and re-reading the entry under it, rather than upgrading mid-fault. Different shape, same hazard removed. That is independent support that the window is real even though I could not make it fire on demand. Happy to re-run the stress with a better repro if anyone has one that lands in the window more reliably. |
Summary
We have a TOCTOU in
mem_ptr()COW path:dataandentrypointers were derived under read lock but directly used after a lock upgrade gap. As another thread can also free/remap the page under this condition, it reliably crashes in_platform_memmoveon iOS.mmapandmemcpyare skipped entirely — no wasted allocation. (Human: I forced CC to do this withultrathink)The bug
Sorry but I don't understand the following fully. But I think since the code was not in a critical region, we can assume for every line of code, some other thread may be also executing any step of it. Hence why we can assume two threads simultaneously doing step 2 and 3, leading to the issue?
Claude Code's analysis
The COW path in
mem_ptr()does a lock upgrade (read → write) to copy a COW page. The old code had this sequence:Between steps 2 and 3, another thread can acquire the write lock first and:
entry)Both
dataandentrybecome dangling pointers. Classic TOCTOU across the lock upgrade.Crash signature
From iOS Simulator crash reports:
_platform_memmovecalled frommem_ptr+772(memory.c:286)VM_ALLOCATEregion (backing data freed/unmapped by another thread)EXC_BAD_ACCESS (SIGSEGV)—KERN_INVALID_ADDRESSNon-deterministic, depends on thread scheduling, but reproduces within seconds under sufficient thread pressure.
The fix
If I am not mistaken, the fix is to get the read lock, then get the write lock, and only if it all checks out in both stages we do the crucial operations such as
data,copy,memcpy, andpt_map?ultrathinkoutcome: whyP_COWis checked twice: The outer check (under read lock) is a performance gate — without it, every non-COW write would needlessly do a lock upgrade. The inner check (under write lock) is the correctness re-validation after the lock gap. Both are required; they serve different purposes.How to reproduce
apk add nodejs npm)How it was tested
On iOS Simulator (iPhone 16 Pro, iOS 18.4) with Node.js 20:
Process note
This fix was developed with Claude Code (Anthropic's AI coding agent). The investigation started from an iOS Simulator crash report showing
_platform_memmovefaulting insidemem_ptr. The lock upgrade gap was identified as a classic TOCTOU pattern onentryanddata. The fix follows the existing pattern in the same function.