* fix(kpm): build with -mcmodel=large so .rodata addressing survives the loader
The KPM ctl0 status/stats replies came back corrupted on a real APatch
(KernelPatch) device — garbled/duplicated string literals ("vpnhide vpnhide",
"backendr", a stray "1 stats" prefix). That made the app's root snapshot
fail to parse the kpm_state section and show "Startup preparation failed",
with no working KPM control channel.
Root cause: the .kpm was built with -mcmodel=small, which emits ADRP
(ADR_PREL_PG_HI21, PC-relative ±4 GB) relocations for .rodata string literals.
KernelPatch loads the module in a vmalloc region that can sit more than 4 GB
from where those literals were assumed, so the relocations overflow and resolve
to garbage addresses. The string-formatting code (vpnhide_format_status/stats,
shared with the .ko) is correct — it's only the .kpm's address materialisation
that breaks. The .ko is unaffected because Kbuild handles module addressing.
Switch the KPM build to -mcmodel=large, which materialises absolute addresses
via MOVW_UABS (no ±4 GB range limit). Verified on a real Pixel 4a (APatch,
4.14): ctl0 status/stats now decode cleanly and the app starts. QEMU KPM
harness still passes 18/18 with no panic on android16-6.12 (the larger .kpm
still boots KernelPatch) and android14-6.1.
* fix(activator): trust kpatch ctl0-read stdout, not its byte-count exit status
The kpatch CLI prints a ctl0 reply to stdout and exits with the supercall's
return value, which for a READ is the reply byte count (e.g. 64), not 0.
run_kpatch_kpm_ctl0_read treated any non-zero exit as failure, so every KPM
status/stats read failed on KernelSU / KPatch-Next and the dashboard showed no
KPM counters. (On APatch the supercall is read directly, so that path was
unaffected — its bug was the .rodata corruption fixed by the -mcmodel change.)
Capture stdout regardless of exit code: the reply text is authoritative, and on
a real error the CLI leaves its buffer empty so stdout is empty. The config path
already tolerates the byte-count exit status (kpatch_ctl0_config_status_ok); this
brings reads in line. Verified on a real Pixel 8 Pro (KSU-Next + KPatch-Next):
'activator state' now returns the clean status/stats reply instead of erroring.
The android12-5.10 DDK image ships clang-12 (r416183b), whose driver adds
CRT startup files + -lc/-ldl to the relocatable `-r` link, which then fails
(crtbegin_dynamic.o / -lc not found). Newer DDK clangs don't, so only that
KMI broke. The .kpm is a freestanding relocatable kernel object that must
not link any CRT/libc — add -nostdlib so every clang version links it
identically. Verified: valid REL aarch64 object with the hook symbols on
clang-12, the newer DDK clangs, and the legacy trixie plain clang.
A plain distro clang (the legacy KPM image) defaults to the host GNU ld,
which can't perform the aarch64 -r relocatable link and fails silently
('linker command failed'). DDK clang bundles lld and defaults to it, so
the GKI path was unaffected. Pin -fuse-ld=lld so both toolchains link the
.kpm identically, and add the lld package to the legacy image.
Pin bmax121/KernelPatch (incl. its recent 4.14 kallsyms fix) under
kmod/third_party/. Makefile defaults KP_DIR to the submodule, so
`make kpm` works out of the box (after submodule init) and CI can build
the .kpm reproducibly.
Compiling the KPM skeleton against bmax121/KernelPatch surfaced two real
API mismatches; fixing them makes `make kpm KP_DIR=...` produce a valid
.kpm ELF (verified: .kpm.info/.kpm.init/.kpm.ctl0/.kpm.exit sections,
metadata populated).
- codegen: emit a self-contained C matcher — local ASCII case-fold, no
<ctype.h>/<string.h>/<stdbool.h>/<linux/*> (KernelPatch's -nostdinc world
has no ctype/types stubs). Same header now compiles unchanged in the .ko,
the KPM, and the host test. Rendered from a triple-quoted template block
instead of line-by-line append (stdlib-only; no jinja). Behaviour
identical — host matcher test still passes 42/42; Rust/Kotlin outputs
unchanged.
- kpm: hook_fargs14_t doesn't exist (max bucket is hook_fargs12_t); rtnl
reads only arg0/arg1, so use hook_fargs12_t + argno=12.
- Makefile: KPM include dirs mirror KernelPatch's own kpms/*/Makefile.
- README: build works today; status updated.
Structural skeleton for a third native backend alongside the kretprobe
.ko, to reach kernels the .ko can't: non-GKI / proprietary kernels with
no headers or Module.symvers (kernel 4.14 — #33; HyperOS 5.4 — #35), and
locked-down module signing. Not yet buildable here (needs the KernelPatch
header tree) and not yet validated — every offset and hook must pass a
QEMU KPM harness (TODO) before anything ships.
- shared/vpnhide_logic.h: backend-agnostic, freestanding filtering logic
(seq-buffer compaction, UID parsing) shared by both the .ko and the KPM;
host unit test in shared/test_vpnhide_logic.c (passes)
- kpm/vpnhide_kpm.c: KPM skeleton — KPM_INIT/CTL0/EXIT, runtime symbol
resolution, two PoC hooks (fib_route_seq_show, rtnl_fill_ifinfo) wired to
the shared logic, with the remaining hook backlog documented inline
- kpm/kver_offsets.h: one source + a runtime per-kver struct-offset table
(one binary for 4.x/5.x/6.x), instead of soranerai's three per-version copies
- kpm/README.md: architecture, KernelPatch API, deploy (KPatch-Next),
safety (inline hooks = panic on wrong offset), status/backlog, credits
- Makefile: add a `kpm` target (clang -r -nostdinc against KP_DIR) that
doesn't require the GKI KERNEL_SRC the .ko build needs
Deliberate fixes vs the community prototypes: per-call state via
fargs->local (not a per-CPU MPIDR stash that races on migration); reuse the
generated matcher (gets the if<N> pattern, #86); do not hook rt_fill_info
(unstable arg->register ABI, proven by the .ko's QEMU harness).
Build system:
- Replace hardcoded paths in Makefile with env vars (KERNEL_SRC, CLANG_DIR)
- Add .env.example and .envrc for direnv-based config
- Simplify build-zip.sh to delegate to make instead of duplicating build command
- Rewrite BUILDING.md: 5-step happy path with direnv, standalone prep as appendix
- Remove redundant quick-reference script and step 7 (manual module.lds hack)
Kernel module (vpnhide_kmod.c):
- Fix fib_route_seq_show hook: save seq_file pointer and buffer position in entry
handler instead of reading regs->regs[0] in return handler (which holds the
return value on arm64, not the original argument). Rewrite buffer scanning as
clean forward iteration with memmove compaction.
- Remove dead SIOCGIFCONF case from dev_ioctl hook (confirmed in kernel source:
SIOCGIFCONF goes through sock_ioctl -> dev_ifconf, not dev_ioctl on GKI 6.1)
- Fix header comment: remove false tcp4_seq_show claim, correct rtnl symbol name
Test app:
- Auto-run checks on launch (LaunchedEffect) for easier adb-driven testing
Unified repository for the complete Android VPN-hiding stack:
- zygisk/ — Rust Zygisk module (inline libc hooks via shadowhook)
- lsposed/ — Kotlin LSPosed module (Java API + system_server hooks)
- kmod/ — C kernel module (kretprobe hooks, invisible to anti-tamper)
CI workflows use path filters to build only the changed component.