Remove the traces our own unloading leaves behind - #161
Merged
Merged
Conversation
JingMatrix
force-pushed
the
linker-unload-trace
branch
from
September 3, 2026 15:17
9583a7a to
b2d09a5
Compare
clean_linker_trace() in run_modules_post asked for "jit-cache-zygisk". The daemon creates the module memfd as "zygisk-module", and has since 5e43a06 renamed it back from the former; b4d31ab caught the same stale name in spoof_virtual_maps but never opened this call site. So wherever a module declines to unload -- 25 processes in one boot here, system_server among them -- the linker kept an soinfo record reading /memfd:zygisk-module (deleted). spoof_virtual_maps hides the mapping; the record stays visible to anyone walking the list. Releasing an soinfo returns its block to the linker's slab allocator, which is strictly LIFO, so the block released last is reissued first. Releasing front to back hands them back in reverse, and dl_iterate_phdr enumerates in allocation order -- an object on a lower block than the one before it is a step backwards that a process which never unloaded anything cannot show. Release the highest block first, in dropSoPath and in the module loop, and the reuse stays ascending. Collect before releasing too: soinfo_free memsets the block, so walking the list while releasing read a zeroed next and stopped after the first hit. The self-unmap was short by the .bss. The ptracer sums the mappings carrying our pathname, and bionic maps the tail of the last PT_LOAD as an anonymous zeromap with no file behind it -- 8192 bytes on arm64, one page on the 32-bit ABIs, stranded in every process we inject into. dropSoPath now reports soinfo::size, and hook_entry prefers it once the base agrees. That sum identifies the library by name once and collects by device and inode after, so a "(deleted)" suffix cannot widen the range we unmap. Carry the whole .llvm suffix: Android 17's twenty-six characters filled the old buffer and left it unterminated, so solinker failed to resolve and the solist path degraded silently. Spoofed regions are named rather than left anonymous.
JingMatrix
force-pushed
the
linker-unload-trace
branch
from
September 3, 2026 15:26
b2d09a5 to
a477233
Compare
HSSkyBoy
pushed a commit
to HSSkyBoy/NyaZygisk
that referenced
this pull request
Sep 6, 2026
clean_linker_trace() asked for a memfd name the daemon no longer creates, so wherever a module declines to unload the linker kept an soinfo record naming a deleted memfd. spoof_virtual_maps conceals the mapping, not the record, and the record is what a walk of the linker's list returns. An soinfo returns to a strictly LIFO slab allocator, so the block released last is reissued first. Releasing our records front to back hands them back in reverse, while dl_iterate_phdr enumerates in allocation order -- a later object on a lower block is a step backwards that a process which never unloaded anything cannot produce. Releasing the highest block first keeps the reuse ascending. Collecting before releasing is required regardless: soinfo_free zeroes the block, so a walk that releases as it goes reads a zeroed next and stops after the first match. The self-unmap measured us by summing mappings that carry our pathname, but the tail of the last PT_LOAD is an anonymous zeromap with no path, so it was excluded and left mapped. soinfo::size is the reservation the linker actually recorded. Also: match the injected library by device and inode, carry the whole .llvm symbol suffix, and name the spoofed regions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
clean_linker_trace()inrun_modules_postasked forjit-cache-zygisk. The daemon creates the module memfd aszygisk-module, and has done since 5e43a06 renamed it back from the former; b4d31ab caught the same stale name inspoof_virtual_mapsand fixed that call site, but this one was never opened. So for every process where a module declines to unload — 25 of them in one boot here, roughly one in six,system_serveramong them — the linker kept an soinfo record whose realpath reads/memfd:zygisk-module (deleted).spoof_virtual_mapshides the mapping, so nothing shows in/proc/<pid>/maps; the record is visible to anyone who walks the linker's list, which is exactly what Demo's solist scan does.Releasing an soinfo hands its block back to the linker's slab allocator, which is strictly LIFO:
free()pushes at the head andalloc()pops it, so the block released last is reissued first. Releasing our records front to back therefore hands them back to the next loads in reverse, anddl_iterate_phdrenumerates in solist order, which is allocation order — an object landing on a lower block than the one before it is a step backwards that a process which never unloaded anything cannot show. Release highest block first, indropSoPathand in the module loop, and the reuse stays ascending. Measured: the step is gone, three runs.dropSoPathcollects before releasing now.soinfo_freememsets the block, andsoinfo::nextlives past the free-list pointers threaded through its first sixteen bytes, so walking the list while releasing read a zeroednextand stopped after the first hit — with several matching records only one ever went.The self-unmap was short by the
.bss. The ptracer measures us by summing the mappings that carry our pathname, and bionic maps the tail of the lastPT_LOADas an anonymous zeromap with no file behind it (ElfReader::LoadSegments): 8192 bytes on arm64, one page on the 32-bit ABIs, stranded in every process we inject into.dropSoPathnow reports the reservation the linker recorded insoinfo::size, andhook_entryprefers it once its base agrees with the range the ptracer handed over.That sum also keyed on a pathname substring. It identifies the library by name once and collects by device and inode after, so a
(deleted)suffix or a second mapping sharing the basename cannot widen the range we later unmap.Carry the whole
.llvmsuffix. Android 17 ships a twenty-digit hash, twenty-six characters with the prefix, which exactly filled the old buffer and left it without a terminator forsnprintfto run off;solinkerthen failed to resolve and the solist path degraded silently.Spoofed regions are named
dalvik-DEX datarather than left anonymous. Note this only defeats a scan that reads the label —PR_SET_VMA_ANON_NAMElets any process name its own anonymous memory, so the name is not evidence either way.