Steps to reproduce/复现步骤
When there is a certain amount of memory pressure, start an application and occasionally trigger it
Expected behaviour/预期行为
Nothing should have happened
Actual behaviour/实际行为
(My agent wrote these)
Symptom
Freshly forked system apps crash ~1s after fork with SIGSEGV inside the injected library:
signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x00000071b77633f0
Cmdline: zygote64 pid: 32023, name: id.printspooler >>> zygote64 <<<
#00 pc 000000000007431c /data/adb/neozygisk/lib64/libzygisk.so
Same libzygisk.so+0x7431c offset reproduced across multiple apps
(com.android.printspooler, com.miui.notification:remote), always during the
soft-restart churn window that follows a system_server crash / zygote restart.
Crashing instruction is stp xzr, xzr, [x12, #8] — a 16-byte zeroing store inside
a loop that compacts the libc atexit array.
Analysis
Offset 0x7431c maps to AtexitArray::recompact() in
loader/src/injector/atexit.cpp (inlined into clean_libc_trace() →
run_modules_post(), which runs in the forked app when all Zygisk modules
unload). The register trace and the adjacent log string
mprotect on atexit array failed with %d: %s (atexit.cpp:56) show:
recompact() first calls set_writable(true, 0, size_), i.e.
mprotect(array_, page_end(size_*sizeof(AtexitEntry)), PROT_READ|PROT_WRITE).
- The
mprotect fails (EINVAL/ENOMEM).
- The failure is only logged — execution continues.
- The compaction loop then executes
array_[src] = {}
(atexit.cpp:25), a write to memory that was never made writable
→ SEGV_ACCERR.
This contradicts the upstream Android bionic implementation this code was
ported from (platform/bionic/libc/bionic/__cxa_atexit.cpp), where the same
mprotect failure calls async_safe_fatal("mprotect failed on atexit array: %m")
and aborts instead of proceeding to write.
A second behavioral divergence in the same function:
|
AOSP bionic |
NeoZygisk |
recompact() early exit |
if (!needs_recompaction()) return; |
logs LOGV and continues anyway |
mprotect failure |
async_safe_fatal(...) (abort) |
PLOGE(...) and continues |
Why intermittent: bionic guarantees array_ is page-aligned because it is always
the result of mmap/mremap (expand_capacity()); those calls also carry the
comment "mremap fails if the source buffer crosses a boundary between two VMAs".
During the restart churn (fresh zygote, atexit subsystem mid-init, injection and
module memfd dlopens perturbing VMA layout), the mprotect range can fail
(e.g. ENOMEM when the requested range crosses a VMA boundary / overshoots the
buffer), and NeoZygisk's non-fatal path then writes into memory it failed to
make writable.
Root implementation/Root 方案
ksu 32525-2
System Module List/系统模块列表
Hybrid mount
HMA-OSS Zygisk
CTS
Vector
NeoZygisk
NeoZygisk version/NeoZygisk 版本
2.4
Android version/Android 版本
16
Version requirement/版本要求
Logs/日志
evidence_neozygisk_atexit_crash.tar.gz
Steps to reproduce/复现步骤
When there is a certain amount of memory pressure, start an application and occasionally trigger it
Expected behaviour/预期行为
Nothing should have happened
Actual behaviour/实际行为
(My agent wrote these)
Symptom
Freshly forked system apps crash ~1s after fork with SIGSEGV inside the injected library:
Same
libzygisk.so+0x7431coffset reproduced across multiple apps(
com.android.printspooler,com.miui.notification:remote), always during thesoft-restart churn window that follows a
system_servercrash / zygote restart.Crashing instruction is
stp xzr, xzr, [x12, #8]— a 16-byte zeroing store insidea loop that compacts the libc atexit array.
Analysis
Offset
0x7431cmaps toAtexitArray::recompact()inloader/src/injector/atexit.cpp(inlined intoclean_libc_trace()→run_modules_post(), which runs in the forked app when all Zygisk modulesunload). The register trace and the adjacent log string
mprotect on atexit array failed with %d: %s(atexit.cpp:56) show:recompact()first callsset_writable(true, 0, size_), i.e.mprotect(array_, page_end(size_*sizeof(AtexitEntry)), PROT_READ|PROT_WRITE).mprotectfails (EINVAL/ENOMEM).array_[src] = {}(atexit.cpp:25), a write to memory that was never made writable
→
SEGV_ACCERR.This contradicts the upstream Android bionic implementation this code was
ported from (
platform/bionic/libc/bionic/__cxa_atexit.cpp), where the samemprotectfailure callsasync_safe_fatal("mprotect failed on atexit array: %m")and aborts instead of proceeding to write.
A second behavioral divergence in the same function:
recompact()early exitif (!needs_recompaction()) return;LOGVand continues anywaymprotectfailureasync_safe_fatal(...)(abort)PLOGE(...)and continuesWhy intermittent: bionic guarantees
array_is page-aligned because it is alwaysthe result of
mmap/mremap(expand_capacity()); those calls also carry thecomment "mremap fails if the source buffer crosses a boundary between two VMAs".
During the restart churn (fresh zygote, atexit subsystem mid-init, injection and
module
memfddlopens perturbing VMA layout), themprotectrange can fail(e.g.
ENOMEMwhen the requested range crosses a VMA boundary / overshoots thebuffer), and NeoZygisk's non-fatal path then writes into memory it failed to
make writable.
Root implementation/Root 方案
ksu 32525-2
System Module List/系统模块列表
NeoZygisk version/NeoZygisk 版本
2.4
Android version/Android 版本
16
Version requirement/版本要求
Logs/日志
evidence_neozygisk_atexit_crash.tar.gz