From 9f2836cf255b73c88eb174f4a1773f65bef39e57 Mon Sep 17 00:00:00 2001 From: Hermes default Date: Thu, 29 Oct 2026 13:57:47 +0100 Subject: [PATCH] fix(android): treat SEGV_MAPERR like SEGV_ACCERR in the write-tracking fault path The guest-heap write tracking (plat_written_pages) protects pages read-only while the guest writes to them. When mprotect() splits a VMA, the kernel can resolve the concurrent guest write against a VMA that is being replaced and report SEGV_MAPERR (si_code=1) even though the page stays mapped and resident. The crash handler only tested SEGV_ACCERR, so the fault was treated as fatal and the game died instead of taking the write-access path. Measured on a Galaxy Z Fold4 (arm64, 4 KiB pages): A memset(352 KiB) on an r--p page, alone 0/20 (always ACCERR) B same + one thread mprotects the same page 20/20 (not recoverable) C same + one thread mprotects a different page 20/20 (not recoverable) D same, one-byte write 3/20 The upstream binary fault trace confirms the kernel itself reports si_code=1 (sig=11 si_code=1 addr=0x7285dc5000, x0=dst x1=0 x2=0x56000, i.e. a memset(dst, 0, 352 KiB)), so the code is not rewritten by the signal chain. wt_fault() is safe on a genuinely unmapped page: mprotect() returns ENOMEM and the 64 KiB neighbourhood attempt fails too, since mprotect() needs the whole range mapped. A real hole therefore still falls through to the diagnostic path below. With the fix, the same build runs 14611 presented frames with 0 crashes, 0 heap faults and 0 signals, against a systematic SIGSEGV at 3 presented frames before. --- runtime/platform/android.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/runtime/platform/android.c b/runtime/platform/android.c index 39acde0c7..64cdc17d3 100644 --- a/runtime/platform/android.c +++ b/runtime/platform/android.c @@ -312,7 +312,14 @@ static void crash_handler(int sig, siginfo_t *si, void *ctx) { if (sig == SIGABRT && depth++ > 0) { signal(SIGABRT, SIG_DFL); abort(); } // (nested: just die) uint64_t fa = (uint64_t)(uintptr_t)si->si_addr; uint64_t pc = uc->uc_mcontext.pc, lr = uc->uc_mcontext.regs[30]; - if (sig == SIGSEGV && si->si_code == SEGV_ACCERR && wt_fault(fa)) return; + // The write tracking protects the heap pages read-only while the guest writes to them, and the kernel can then + // report the fault as SEGV_MAPERR (the address is not mapped) even though the page stays mapped and resident: an + // mprotect splitting the VMA races with the guest write, and the access resolves against the VMA being replaced. + // Measured on the device: 20/20 MAPERR with a concurrent mprotect, 0/20 without one. Both codes mean the same + // thing here (the page needs its write access back), and wt_fault() fails harmlessly on a page that is really + // unmapped (mprotect returns ENOMEM, and so does the 64 KB neighbourhood attempt), so a real hole still falls + // through to the diagnostic path below. + if (sig == SIGSEGV && (si->si_code == SEGV_ACCERR || si->si_code == SEGV_MAPERR) && wt_fault(fa)) return; if (rt_fault_watch(fa, pc)) return; if (sig == SIGSEGV && fa >= HEAP_BASE && fa - HEAP_BASE < HEAP_MAX) { // (see wt_setup) char m[96];