# Extension verification — kernel-level weapons (templates, gates, and a closed door) Covers the extension pass that added `scripts/kernelsu_syscall_mask.py` and the `§4a` section of `references/kernel-and-environment-hardening.md`. Strength labels as defined in this directory's README: **observed** (exact command + output here), **inferred** (follows from observed facts, step not executed), **unverified** (assumed or externally reported, not independently confirmed). Reference environment: Windows 11 host, Python 3.14.0; `` = rooted Android 11 (API 30) arm64-v8a, Magisk alpha, no APatch/KernelPatch. **Headline: the generator, the gate table and the userspace module skeleton are measured. No kernel-side artifact was compiled or loaded anywhere, and every route is closed on the reference device. That is the deliverable — a template plus a measured statement of the door.** --- ## 1. Kernel gate, measured on the device — observed ``` $ adb shell 'uname -r; cat /proc/version' 4.14.186+ Linux version 4.14.186+ (nobody@android-build) (Android (6443078 based on r383902) clang version 11.0.1 (https://android.googlesource.com/toolchain/llvm-project b397f81060ce6d701042b782172ed13bee898b79), LLD 11.0.1 (/buildbot/tmp/tmp6_m7QH ...)) #1 SMP PREEMPT Wed Mar 30 23:32:42 CST 2022 ``` 4.14.186 is below the 5.10 GKI gate for the eBPF route, and the device carries no KernelPatch-patched image. Read-only query; no device lock was needed and no state was changed. ## 2. Host toolchain probe — observed ``` $ python skills/apk-reverse/scripts/kernelsu_syscall_mask.py gates == kernel-side routes and their gates == eBPF tracepoint/kprobe GKI kernel 5.10+ (Android 12+) why: Android's BPF/tracing support as bpftrace-class tooling and stackplz expect it is a GKI-era feature; BTF is required for CO-RE style probes. check: uname -r -> expect 5.10.x or newer; ls /sys/kernel/btf/vmlinux KernelPatch / APatch KPM KernelPatch-patched boot image + kpm toolchain why: KPMs load through kpimg injected into the kernel image's payload segment; building one needs a bare-metal ARM64 compiler (aarch64-none-elf-gcc), not the NDK. check: ls /data/adb/ | grep -i apatch ; which aarch64-none-elf-gcc Out-of-tree LKM Kernel source matching the device's exact version + vermagic why: A module built against a different kernel revision is refused at load time on a mismatched vermagic, and the device's kernel source is frequently not published at all. check: uname -r ; look for a matching kernel source tree for the device seccomp-BPF (for contrast) any modern kernel, but it is not this route why: seccomp can make a syscall FAIL (SECCOMP_RET_ERRNO/TRAP); it cannot rewrite the CONTENT a successful read returns. It closes doors, it does not paint them -- which is the whole requirement for a spoofed /proc read. check: n/a -- this is a capability ceiling, not a version check == host probe (what can be checked from here) == bare-metal ARM64 compiler: NOT FOUND the KPM route cannot be built from this host as configured ndk-build: NOT FOUND make: NOT FOUND A kernel source tree, a patched boot image and the device's own kernel headers cannot be checked from the host; they are device facts. ``` The three `NOT FOUND` lines are the measured reason the KPM route cannot be built from here, and they are also consistent with `ENV.md`'s record that the machine has no Android NDK. The LKM route additionally needs kernel source that does not exist for this device. ## 3. `generate` — observed ``` $ python skills/apk-reverse/scripts/kernelsu_syscall_mask.py generate \ --out tools/_work/t5-vmp/sysmask --package '' generated module scaffold in tools/_work/t5-vmp/sysmask module.prop 165 B customize.sh 166 B post-fs-data.sh 830 B service.sh 467 B uninstall.sh 298 B config/syscall_mask.json 1079 B kpm/syscall-mask.c 8096 B kpm/Makefile 880 B lkm/syscall-mask.c 2434 B ebpf/syscall_mask.bpf.c 4338 B README.md 3752 B STATUS: the userspace module is installable; every kernel-side file is an UNVERIFIED template. No kernel code was compiled or loaded. ``` `module.prop`, as generated (the package identity is the `` placeholder by design — this file carries no real package name): ``` id=syscall-mask name=Syscall Mask version=0.1.0 versionCode=1 author=apk-reverse description=config-driven syscall return masking (openat/read/stat on /proc/self/*) ``` The configuration table rendered into the KPM template from `config/syscall_mask.json`, so the rule set is data rather than hand-edited C: ``` static const struct mask_rule rules[] = { { "/proc/self/maps", 0, MASK_DENY, -2, 0 }, { "/proc/self/maps", 0, MASK_FAKE_SIZE, -2, 0 }, { "/proc/self/status", "TracerPid", MASK_FILTER_LINE, -2, 0 }, { "/proc/self/status", "TracerPid", MASK_FILTER_LINE, -2, 0 }, }; ``` And the KPM lifecycle surface, which is the part an interface drift would break first: ``` int kfunc_def(strncpy_from_user)(char *dst, const char __user *src, long count); /* fp_hook_syscalln(__NR_openat, before_openat, 0, 0); */ /* fp_hook_syscalln(__NR_newfstatat, before_newfstatat, 0, 0); */ /* fp_hook_syscalln(__NR_read, before_read, 0, 0); */ KPM_INIT(mask_init); KPM_EXIT(mask_exit); ``` Those names (`KPM_NAME` / `KPM_INIT` / `KPM_EXIT`, `kfunc_def`, `fp_hook_syscalln`, `hook_fargs4_t`, `syscall_argn`, `args->skip_origin`, `args->ret`) are taken from a public KPM development write-up rather than invented — blackr0ck, *APatch KPM 开发:绕过 DirtySepolicy / Duck Detector 的 SELinux Root 检测*, [bbs.kanxue.com/thread-291665.htm](https://bbs.kanxue.com/thread-291665.htm), fetched 2026-02. **They are still `unverified` here**: nobody compiled against a `kpmodule.h` in this pass, and the generated template says so in its own header for exactly that reason. The registration calls are left **commented out** in the template. A generated file that arms a kernel hook by default is a file that arms a kernel hook the first time somebody runs `make` in a hurry. ## 4. `verify` — observed ``` $ python skills/apk-reverse/scripts/kernelsu_syscall_mask.py verify --dir tools/_work/t5-vmp/sysmask == verifying tools/_work/t5-vmp/sysmask == module.prop: author=apk-reverse, description=config-driven syscall return masking (openat/read/stat on /proc/self/*), id=syscall-mask, name=Syscall Mask, version=0.1.0, versionCode=1 config: 4 rule(s), actions=['deny', 'fake_size', 'filter_line'] kpm target present: Makefile, syscall-mask.c status: UNVERIFIED TEMPLATE -- not compiled, not loaded lkm target present: syscall-mask.c status: UNVERIFIED TEMPLATE -- not compiled, not loaded ebpf target present: syscall_mask.bpf.c status: UNVERIFIED TEMPLATE -- not compiled, not loaded kpm/syscall-mask.c: KPM_* lifecycle macros present, kfunc_def not extern == 0 problem(s): structure is consistent == (this says nothing about whether the kernel side compiles or loads) ``` `verify` is deliberately read-only and deliberately narrow: it checks `module.prop` fields, the rule JSON, and the one static trap in the KPM template that has a documented consequence (an `extern` `kfunc_def` becomes `*UND*` and the loader refuses the module). It states on its last line what it does **not** establish, because "0 problems" on a template is the easiest sentence in this file to over-read. ### 4a. A false positive this check produced, and its fix — observed The first run reported: ``` == 1 problem(s) == - syscall-mask.c declares kfunc_def with `extern`, which the loader rejects as an unknown symbol verify rc=1 ``` The template does not declare it with `extern` — its comment *explains* why `extern` is forbidden, and the check's own regex ran from that prose across to the real declaration's semicolon. A linter that reads comments as code produces reports nobody can act on. The fix strips `/* */` and `//` comments before any token check (`_strip_c_comments`). Both the false positive and the fix are recorded here rather than quietly repaired, because the failure mode — a check that fires on its own documentation — is one a reader will meet again. ## 5. The userspace module's ceiling — observed by construction, and worth stating plainly `post-fs-data.sh` and `service.sh` in the generated skeleton do not load anything. That is not an omission: a KernelSU/Magisk/APatch-userspace module runs as root in the normal world and cannot change what a syscall returns. The generated `README.md` says this in its first paragraph, and `post-fs-data.sh` carries the reasoning in a comment: ``` # The kernel side (if you built and loaded one) owns the syscall table; this # script's whole job is to make the configuration visible to it and to leave a # record that the module actually ran. Do not put a syscall hook here: this is # userspace, and nothing here can change what openat/read/stat return. ``` The uninstall hook likewise refuses to unload a kernel module, because a silent unload during package removal is how a device ends up in a boot loop. ## 6. eBPF route — gate table and the capability ceiling | Route | Gate | Check on the device | Status here | |---|---|---|---| | eBPF tracepoint/kprobe | GKI 5.10+ with tracing/BTF | `uname -r`; `ls /sys/kernel/btf/vmlinux` | **closed** — device is 4.14.186 | | seccomp-BPF | any modern kernel | n/a | not this route: it can only fail a syscall, not rewrite a successful read | | KernelPatch / APatch KPM | patched boot image + `aarch64-none-elf-gcc` | `ls /data/adb/ \| grep -i apatch`; `which aarch64-none-elf-gcc` | **closed** — neither present; patch-the-boot-image risk declined | | Out-of-tree LKM | kernel source matching vermagic | `uname -r` + a matching source tree | **closed** — no source for this device | The probe template (`ebpf/syscall_mask.bpf.c`) is a `sys_enter_openat` tracepoint observer with a uid filter map and a ringbuf event, and it documents two dead ends in the file itself: `bpf_override_return()` only applies to `ALLOW_ERROR_INJECTION` functions, which raw syscall entries are not, and a tracepoint cannot rewrite a result at all. **A tracepoint observes; it does not spoof.** The honest eBPF shape for this job is therefore observation — which is still worth having, because `§5` of the reference file requires you to establish whether the target uses a raw `svc` or a libc call *before* designing any spoof. ## 7. What was not done - **No kernel-side code was compiled.** Not the KPM, not the LKM, not the eBPF program. No `kpmodule.h`, no kernel headers, no `bpftool`, no bare-metal ARM64 compiler on this host. - **Nothing was loaded, flashed or activated on ``.** No boot-image patch, no module load, no kernel modification. The task forbids it for good reason: a kernel module that faults takes the device down before it can report why. - **The KPM API surface is `unverified`.** The macro and helper names follow a public write-up; the argument arity of `fp_hook_syscalln` in particular is marked `TODO(verify-on-device)` in the generated source rather than guessed, because a wrong arity in a hook registration is a load-time failure at best. - **The struct-stat size offset is a TODO, not a value.** The `newfstatat` handler deliberately does not carry a magic offset: it is ABI-specific and copying one from a different architecture is how a spoof corrupts an unrelated field. - **`verify` establishes structure, not capability.** Its own output says so.