Repository navigation
feat(libc): trap guest reads of write-watched pages on macOS - #13442
Conversation
|
@Gonic0 retryUntrapped shares one 16-retry budget per page/generation across all threads. If more than 16 readers fault while a page is trapped, then reach the handler after the resolver has restored it, each reader consumes that same budget; the 17th legitimate read returns false and is forwarded to the previous SIGBUS/SIGSEGV handler. The new thread test uses only four threads, so it cannot catch this. Please distinguish repeated unresolvable faults from separate delayed readers and add a deterministic many-reader regression, while keeping the truncated-backing fault bounded. Also, the hosted write-watch job will stop at configure on this head until #13220 lands (main still registers the execute-fault/read-fault block twice), so could the PR body list #13220 under "Depends on"? |
|
Right: the budget was per page, so the delayed readers of one untrap shared it and the 17th was passed on. The new The body now lists #13220 under Depends on. AI-assisted: yes |
|
Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.
|
|
@Gonic0 The new per-thread key and 32-reader regression address the original delayed-reader failure. The 64-entry LRU cache still loses the persistent-fault bound under pressure: with 65 threads repeatedly faulting on one untrapped page in round-robin order, each thread evicts another entry before its next attempt, so every count resets to 1. A source model accepts all 2,600 deliveries across 40 rounds. Could you preserve the bound when entries are displaced and add a regression above the cache capacity, while retaining the legitimate delayed-reader test and truncated-backing negative control? This is a source scheduling finding, not a native macOS reproduction. |
|
Right: past 64 threads, the LRU table evicted each thread's entry before its next attempt. The new AI-assisted: yes |
034f633 to
f476c01
Compare
GuestWriteWatchTrapReads takes all access to the pages of a range until the resolver set with GuestWriteWatchSetReadTrapResolver has run for it. A fault on a trapped page waits while the resolver runs on the watch's own thread; host writes and GuestWriteWatchProtect run it first on the calling thread. Collections keep trapped pages inaccessible, and registering or unregistering memory drops its traps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fault that reaches the handler after its page was given back is retried. The budget for that was shared by every thread faulting on the page, so the 17th delayed reader of one untrap was passed on as a crash. Each thread now has its own budget per page and untrap, which still bounds a fault the page's protection does not explain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A caller that traps the same range at every label then needs no record of its own of which ranges are trapped, which registering memory again could leave stale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-thread retry entries are a 64-entry cache. More threads faulting in turn on a page whose protection does not explain the fault evict each other's entries before their next attempt, so each count restarted at 1 and the faults were retried forever. Retries between two untraps are now also counted in total, up to 64 times the per-thread budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f476c01 to
21e65cc
Compare
|
Rebased onto current Added one commit: fault-ahead unprotected the following pages in |
What
The macOS write watch gains read traps.
GuestWriteWatchTrapReads(address, bytes)takes all access to the pages of a watched range (PROT_NONE) until the resolver set withGuestWriteWatchSetReadTrapResolverhas run for it.GuestWriteWatchReleaseReadsremoves a trap without running the resolver. On Linux and Windows the three exports return false or do nothing.Nothing sets read traps yet, as with
GuestWriteWatchProtectionChanged. The intended user is a follow-up in libSceAgcDriver:StorageTexture::StoreReadbackBeforeLabelwrites back every pending linear or small storage image at each label, whether or not the CPU reads it. With traps, the write-back can wait until a guest thread touches the memory. On PPSA21564 (ASTRO BOT 01.007.000), during the logos before the intro, skipping those write-backs (APS5_NO_LABEL_READBACK_STORE=1) gives 43.1 and 43.2 FPS against 37.6 and 37.5 with them, alternated, and the image stays intact. I then trapped those ranges with a temporary handler, without writing them back. In 75 s, every fault came from the emulator's own reads and writes, none from guest code. During the logos there were no faults. That is one title and one phase, so the follow-up keeps the write-back as the fallback. That follow-up is #13499. It traps those ranges at the label instead of writing them back, releases a trap whenFlushPendingstores its image, and resolves a trap through the host-read hook. In the same scene it gives 44.0 and 44.3 FPS against 38.1 and 37.1, alternated, with the same image.How it works:
0x80). The traps themselves are kept as exact ranges, so two images sharing a page each keep their own trap, and releasing one leaves the page trapped for the other.record(). The faulting thread hands the page to a resolver thread and waits on a Mach semaphore. The resolver cannot run on the faulting thread: the handler is installed withoutSA_NODEFER, and a resolver storing into write-watched pages would raise the blocked signal again.PROT_NONE.GuestWriteWatchBeginHostWrite,GuestWriteWatchProtectandGuestWriteWatchProtectionChangedrun the resolver for the traps they cover first, on the calling thread, since a system call cannot write aPROT_NONEpage and a new protection would erase the trap. A resolver may itself start a host write over its own range; the driver brackets GPU imports that way.#7192 adds
GuestWriteWatchHandleFault, which callsrecord()directly; if both land, that entry point needs the same check for trapped pages first. The macOS write-watch entry indocs/dev/debt/libc.mdgets the gaps:EFAULT;Tested
Apple M5 Pro, macOS 27.2. Configuring
core/libs/tests/write_watchonmainfails on macOS because two test names are registered twice; #13220 fixes that. So I builtwrite_watch_testsfrom this branch withclang++ -std=c++20 -O2 -Wall -Wextra, for x86-64 (run under Rosetta 2) and for arm64. I also built it with the project's CMake in a tree with the open macOS port PRs, where these files are identical. There,ctest -R '^write_watch_'passes 23 of 23. libc also builds there with the project's flags.New test modes:
read-trap:PROT_NONE;read()from a pipe fills the page;GuestWriteWatchProtectresolves the trap and applies its protection;GuestWriteWatchTrapReadsfails without a resolver, on unwatched memory and under a host write.read-trap-threads: four threads read 32 trapped pages, two at a time on the same page. The resolver stores through a second mapping of the same file, as a GPU stores through a host import. Every read sees the resolver's bytes and each trap is resolved exactly once.read-trap-fault: with traps set, a read of a watched file mapping whose backing was truncated still kills the process withSIGBUSinstead of being retried forever.read-trap-fault-bound: 66 threads read a watched file mapping whose backing was truncated, with a Mach exception port on each. The test passes their faults on as signals strictly in turn, so each thread's retry entry is evicted before its next fault, for 40 rounds. The process still dies withSIGBUS.read-trap-late: 32 threads read a trapped page with a Mach exception port set on each, which holds their faults until all 32 have faulted. The page is then resolved, and only after that are the faults passed on as signals. All 32 handlers find the page given back and retry, and every read sees the resolver's bytes.All write-watch modes pass on both architectures.
read-trap,read-trap-threads,read-trap-fault,read-trap-lateandread-trap-fault-boundpassed 10 times each on both, andread-trap-threads30 more times on each. The other tests in that tree that use the write watch,guest_arena_*,guest_memory_lifetime*,agc_driver_guest_memory_lifetime*andguest_memory_pool_stats, pass.guest_memorydid not build there, for asceKernelMunmapcall unrelated to this change.Negative controls, each of which fails a test:
record()before the trap check: "a write to a trapped page does not land after the resolver's bytes";read-traphangs until its alarm;read-traphangs until its alarm;read-trap-threadsdies withSIGBUS;73afc098f1: "32 read faults that arrive after their page was given back are not all retried";Not tested on Linux or Windows, where the change adds three exports that return false or do nothing.
Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)🤖 Generated with Claude Code