Skip to content

Stage a module's native libraries where system_server may map them - #830

Open
JingMatrix wants to merge 1 commit into
masterfrom
fix/729-native-libs-system-server
Open

Stage a module's native libraries where system_server may map them#830
JingMatrix wants to merge 1 commit into
masterfrom
fix/729-native-libs-system-server

Conversation

@JingMatrix

@JingMatrix JingMatrix commented Jul 31, 2026

Copy link
Copy Markdown
Owner

A module scoped to system_server cannot load its own native library:

java.lang.UnsatisfiedLinkError: dlopen failed: couldn't map
  ".../com.tsng.hidemyapplist-.../base.apk!/lib/arm64-v8a/libhmahook.so"
  segment 2: Permission denied

The dlopen is Android's rather than ours. The module calls System.loadLibrary, and the only thing this framework contributes is the search path, which both loaders build purely out of in-APK entries of the form <apk>!/lib/<abi>. bionic opens the APK, maps the read-only segments, and then fails on the first one that wants PROT_EXECElfReader::MapSegment, where the %m in that message is strerror(errno), so it is EACCES spelled out. The kernel asks for FILE__EXECUTE exactly when a mapping requests PROT_EXEC, and /data/app is apk_data_file, a type AOSP lets system_server read, open, getattr and map but never execute. appdomain holds x_file_perms on that same type, which is why the identical library loads in every app process and fails only here. The Android 15 and 16 trees agree on all of it.

Granting the missing permission is not open to us: system_server.te carries neverallow system_server data_file_type:file no_x_file_perms, and apk_data_file is a data_file_type.

What this does instead needs no new rule. xposed_data is already declared a plain file_type, deliberately outside data_file_type, and the allow * xposed_data {file dir} * we already ship expands over every domain in the policy through magiskpolicy's match-all operator, system_server among them, with execute and map included; the daemon already creates /data/misc/<uuid> and labels it. So FileSystem.stageNativeLibraries copies a system_server-bound module's lib/<abi>/*.so into <misc>/lib/<package>/ at 0644 under 0711 directories, and the loader puts that directory first on the search path when it is loading into system_server. Everything else keeps the in-APK path it already had.

The copy is keyed to the APK's size and mtime and to the framework's versionCode, so a module that updates is re-extracted rather than leaving system_server running native code it has since replaced, and directories whose module is no longer bound for system_server are dropped. Staging deliberately pays no attention to moduleLibraryNames: that list only names the libraries whose native_init we are asked to call, and nothing stops a module loading its own without declaring any. HMA ships no native_init.list at all, so gating on it would have fixed nothing for the module that prompted this.

While here, the modern loader registers the declared native entrypoints before the entry classes run instead of after, which is what the legacy loader has always done. A module that loads its library from a constructor or from onModuleLoaded was registering it after the do_dlopen hook had already missed it.

Verified on an SM-A536B, Android 15, enforcing, KernelSU with NeoZygisk, running HMA-V3.8.2.r517 scoped to system. Before the change the kernel says it twice, once as the denial and once as the syscall it came from:

avc:  denied  { execute } for  comm="HMA-ServiceInit"
  path=".../com.tsng.hidemyapplist-.../base.apk"
  scontext=u:r:system_server:s0 tcontext=u:object_r:apk_data_file:s0
  tclass=file permissive=0

type=1300 ... arch=c00000b7 syscall=222 success=no exit=-13 a2=5 a3=12
  comm="HMA-ServiceInit" exe="/system/bin/app_process64" subj=u:r:system_server:s0

On arm64 syscall 222 is mmap, a2=5 is PROT_READ|PROT_EXEC, a3=0x12 is MAP_PRIVATE|MAP_FIXED, and exit=-13 is EACCES; readelf puts the R E PT_LOAD of libhmahook.so at program-header index 2, which is where the reported "segment 2" comes from. After it, the same module in the same process loads out of the staged copy:

V/VectorNative: do_dlopen hook triggered for library:
  '/data/misc/<uuid>/lib/com.tsng.hidemyapplist/libhmahook.so'

with no execute denial anywhere in the boot, against two before, and HMA itself reporting Module Activated [3.8.2-517] and System service running [104]. A small test module written for this reproduces the same failure in system_server on master, loads from the staged directory on this branch, and in an ordinary app process, in the same boot, still loads straight out of its APK.

)

A module loaded into system_server cannot dlopen a library out of its own
APK. Everything under /data/app is apk_data_file, and while system_server
may read and map such a file it may not execute it, which AOSP states
outright and forbids granting: "Executable files in /data are a
persistence vector". Every app domain does hold that permission, so the
same module loads the same library without trouble in an ordinary process
and fails only here, with the linker refusing the first PROT_EXEC mapping:

  dlopen failed: couldn't map ".../base.apk!/lib/arm64-v8a/libhmahook.so"
  segment 2: Permission denied

The way past it is not a new rule but the one this module already ships.
xposed_data is a type we declare ourselves, outside the data_file_type
attribute that neverallow is written against, and the existing
`allow * xposed_data {file dir} *` already reaches every domain,
system_server included. So the daemon now copies a system_server-bound
module's libraries into the misc directory it already owns and labels,
and hands the loader that directory to search first. Nothing is staged
for any other process: they can execute straight out of /data/app, and
staging for them would buy nothing but disk.

The copy is keyed to the APK's size, mtime and the framework version, so
an updated module is re-extracted rather than left running superseded
native code, and directories belonging to modules that are no longer
bound for system_server are dropped.

Staging deliberately ignores moduleLibraryNames: that list only names the
libraries whose native_init we are asked to call, and a module may load
its own libraries without declaring any -- the module that prompted this
does exactly that.

While here, register the declared native entrypoints before the entry
classes run instead of after. A module may load its libraries from its
constructor or from onModuleLoaded, and an entrypoint recorded afterwards
is one the dlopen hook has already missed. The legacy loader has always
done it in this order.
@JingMatrix JingMatrix linked an issue Jul 31, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Latest version of HMA does not work with Vector

1 participant