Skip to content

SIGSEGV (SEGV_ACCERR) in AtexitArray::recompact() on Android 16(HyperOS) #156

Description

@GlowingBrick

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:

  1. recompact() first calls set_writable(true, 0, size_), i.e.
    mprotect(array_, page_end(size_*sizeof(AtexitEntry)), PROT_READ|PROT_WRITE).
  2. The mprotect fails (EINVAL/ENOMEM).
  3. The failure is only logged — execution continues.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions