Skip to content

Hold released 1-16 MB guest allocations for 2 s before unmapping - #146

Merged
willfaust merged 1 commit into
willfaust:mainfrom
bahacan16:pr/delayed-release
Oct 4, 2026
Merged

willfaust merged 1 commit into
willfaust:mainfrom
bahacan16:pr/delayed-release

Conversation

@bahacan16

Copy link
Copy Markdown
Contributor

Problem

Ghost of Tsushima crashed in every run at the same kind of place: a job worker faults reading a 1-2 MB block that had just been freed. One run instead crashed in Metal's command-buffer completion handler, releasing a corrupted object (objc_release on garbage), and a Wine thread crashed on the same value.

Cause

A use-after-free race in the game's job system as it runs here: one worker MEM_RELEASEs a 1-2 MB block the main thread allocated while other workers are still memcpying out of it (Madeira's view history: the view was deleted 3-8 ms before the fault), with fastsync on or off. Freed guest-band VA is handed out again at once -- to the game or to Metal / malloc -- so the late readers either fault or scribble over someone else's objects. The root cause of the race (job refcount / wait ordering under the emulator) is not found yet.

Change

build/ntdll-unix/virtual_ios.c, NtFreeVirtualMemory (current process only): a whole-view MEM_RELEASE (size 0, page-aligned base) of a private (is_view_valloc, not SEC_FILE / SEC_IMAGE / system) 1-16 MB allocation in the 64-bit guest band (0x7000000000..0x7c00000000) returns success at once, but the mapping stays committed for MADEIRA_FREE_DELAY_MS (default 2000 ms; 0 turns it off) and is released afterwards, from the next NtFreeVirtualMemory call after the delay. Above 128 MB held, entries are released at once until 128 MB remain; at most 256 entries are held. A second release of a held base returns STATUS_MEMORY_NOT_ALLOCATED, as for a freed region. Late readers find valid memory, and nobody else gets that VA meanwhile. Log: [free-delay] #N release of ... held for 2000 ms (first 8, then every 1024th). Catalog row added.

Host test tests/host/check-free-delay.py: compiles the production helpers against stub views / clock / NtFreeVirtualMemory and checks which releases are held, the double release, the delayed and capped release through the bypass, MADEIRA_FREE_DELAY_MS=0, and the call site.

Evidence

iPhone 17 Pro Max, iOS 27, Ghost of Tsushima: before, every run died on the freed-while-used fault (or the Metal completion-handler crash); with the delay the opening scene played for about 3 minutes, horse riding and the hand-over to the player worked, the menu opened and the game saved; [free-delay] held 8 releases of 1-8 MB in that run. The delay stayed on by default in the fork afterwards for God of War, GTA V Enhanced and 32-bit games without issues attributed to it.

Notes / risks

  • This is a mitigation, not a root-cause fix; MADEIRA_FREE_DELAY_MS=0 reproduces the race for whoever looks for it.
  • Memory freed by the game stays mapped up to 2 s longer (<= 128 MB at a time), which costs footprint under memory pressure.
  • During the delay the VA still shows as allocated: a VirtualQuery of a held block reports it committed, and a fixed-address allocation over it would fail until it is released. No such case was seen.
  • The fork commit 80bcf2d ("place 64-bit views above the 32-bit windows when the band below is full", same Ghost of Tsushima runs) is not ported: upstream's lazy WoW64 windows reserve no placeholder in a 64-bit-only session, so the band confinement it fixed does not happen there, and upstream deliberately keeps the side above the lowest slots as the [cage] holdback.
  • Not compiled for iOS here; a differential clang -fsyntax-only of virtual_ios.c against upstream shows no new diagnostics beyond the stubbed-header environment. check-config-catalog.py needs the submodules and was not run; the row was rendered with the generator's own code.

🤖 Generated with Claude Code

Claude-Session: https://claude.ai/code/session_0189oLHghpaYKLk4f786a6bc

## Problem

Ghost of Tsushima crashed in every run at the same kind of place: a job worker
faults reading a 1-2 MB block that had just been freed. One run instead
crashed in Metal's command-buffer completion handler, releasing a corrupted
object (`objc_release` on garbage), and a Wine thread crashed on the same
value.

## Cause

A use-after-free race in the game's job system as it runs here: one worker
`MEM_RELEASE`s a 1-2 MB block the main thread allocated while other workers are
still `memcpy`ing out of it (Madeira's view history: the view was deleted 3-8 ms
before the fault), with fastsync on or off. Freed guest-band VA is handed out
again at once -- to the game or to Metal / malloc -- so the late readers either
fault or scribble over someone else's objects. The root cause of the race
(job refcount / wait ordering under the emulator) is not found yet.

## Change

`build/ntdll-unix/virtual_ios.c`, NtFreeVirtualMemory (current process only):
a whole-view `MEM_RELEASE` (size 0, page-aligned base) of a private
(`is_view_valloc`, not SEC_FILE / SEC_IMAGE / system) 1-16 MB allocation in the
64-bit guest band (0x7000000000..0x7c00000000) returns success at once, but the
mapping stays committed for `MADEIRA_FREE_DELAY_MS` (default 2000 ms; 0 turns
it off) and is released afterwards, from the next NtFreeVirtualMemory call
after the delay. Above 128 MB held, entries are released at once until 128 MB
remain; at most 256 entries are held. A second release of a held base
returns STATUS_MEMORY_NOT_ALLOCATED, as for a freed region. Late readers find
valid memory, and nobody else gets that VA meanwhile. Log: `[free-delay] #N
release of ... held for 2000 ms` (first 8, then every 1024th). Catalog row
added.

Host test `tests/host/check-free-delay.py`: compiles the production helpers
against stub views / clock / NtFreeVirtualMemory and checks which releases are
held, the double release, the delayed and capped release through the bypass,
`MADEIRA_FREE_DELAY_MS=0`, and the call site.

## Evidence

iPhone 17 Pro Max, iOS 27, Ghost of Tsushima: before, every run died on the
freed-while-used fault (or the Metal completion-handler crash); with the delay
the opening scene played for about 3 minutes, horse riding and the hand-over
to the player worked, the menu opened and the game saved; `[free-delay]` held
8 releases of 1-8 MB in that run. The delay stayed on by default in the fork
afterwards for God of War, GTA V Enhanced and 32-bit games without issues
attributed to it.

## Notes / risks

- This is a mitigation, not a root-cause fix; `MADEIRA_FREE_DELAY_MS=0`
  reproduces the race for whoever looks for it.
- Memory freed by the game stays mapped up to 2 s longer (<= 128 MB at a
  time), which costs footprint under memory pressure.
- During the delay the VA still shows as allocated: a `VirtualQuery` of a
  held block reports it committed, and a fixed-address allocation over it
  would fail until it is released. No such case was seen.
- The fork commit 80bcf2d ("place 64-bit views above the 32-bit windows when
  the band below is full", same Ghost of Tsushima runs) is not ported:
  upstream's lazy WoW64 windows reserve no placeholder in a 64-bit-only
  session, so the band confinement it fixed does not happen there, and
  upstream deliberately keeps the side above the lowest slots as the [cage]
  holdback.
- Not compiled for iOS here; a differential `clang -fsyntax-only` of
  virtual_ios.c against upstream shows no new diagnostics beyond the
  stubbed-header environment. `check-config-catalog.py` needs the submodules
  and was not run; the row was rendered with the generator's own code.

---
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0189oLHghpaYKLk4f786a6bc
Signed-off-by: bahacan16 <190844990+bahacan16@users.noreply.github.com>
willfaust pushed a commit that referenced this pull request Oct 4, 2026
Squashed from #146.

Signed-off-by: bahacan16 <190844990+bahacan16@users.noreply.github.com>
willfaust added a commit that referenced this pull request Oct 4, 2026
MADEIRA_FREE_DELAY_MS (#146) now defaults to 0. It works around one game's
race and holds up to 128 MB more in every game, against the 4096 MB jetsam
limit of 6 GB iPhones; set env.MADEIRA_FREE_DELAY_MS = 2000 in madeira.cfg
or a game's own config to use it. The duplicate check runs again under the
lock before a block is held: two threads releasing the same base could both
hold it, and the drain then freed it twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@willfaust willfaust closed this Oct 4, 2026
@willfaust willfaust reopened this Oct 4, 2026
@willfaust
willfaust merged commit a70caf9 into willfaust:main Oct 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants