# builtin — in-tree (compile-time) native backend A fourth native backend for vpnhide, alongside `kmod` (.ko / kretprobes), `kpm` (KernelPatch / inline hooks), and `zygisk` (libc hooks). `builtin` compiles the VPN-hiding logic **directly into the kernel** through source call-site hooks — **no loadable module, no kprobes**. It is for kernels where the `.ko` cannot run or cannot hook reliably: - `CONFIG_MODULES=n` / `CONFIG_INTEGRATE_MODULES=y` — `insmod` is a silent no-op; - `CONFIG_KPROBES=n` — no kretprobes to attach; - whole-program LTO (e.g. GCC `CONFIG_LTO_GCC=y`) — hook-target symbols are mangled (`dev_ioctl.lto_priv.0`, `sk_setsockopt.constprop.0`, …), so symbol lookup misses even when kprobes exist. **Audience: people who build their own kernel.** vpnhide ships the patch set and the driver; a kernel maintainer applies it and enables `CONFIG_VPNHIDE=y`. We do **not** distribute prebuilt kernels. The app detects and configures any in-tree build, whoever produced it. ## What is shared vs. new (why this is not a rewrite) vpnhide already separates four layers; `builtin` only introduces a third *attach mechanism* for the same logic. This mirrors what KPM already did. | Layer | kmod | kpm | **builtin** | shared? | |---|---|---|---|---| | Attach mechanism | kretprobe | inline hook | **source call-site + `CONFIG_VPNHIDE`** | no — its own | | Filter logic | `kmod/shared/vpnhide_logic.h` + `kmod/generated/*` | same | **same (vendored at apply)** | **yes, verbatim** | | Config transport | `/proc/vpnhide_ctl` | ctl0 supercall | **`/proc/vpnhide_ctl` (identical)** | reuses kmod's | | Wire format | protocol crate (control v2 / telemetry v1) | same | **same** | **yes** | So the app/activator wire path for `builtin` is byte-identical to `kmod`; only the `status backend` id differs (`VPNHIDE_BACKEND_BUILTIN = 4`, added append-only to `data/hooks.toml`). ## Layout ``` builtin/ security/vpnhide/ # the in-tree driver (copied to /security/vpnhide/) core.c # brain: config, stats, /proc/vpnhide_ctl, init (lifted from the .ko) hook_iface.c # should_hide_dev/ifname: ioctl + SIOCGIFCONF + all dump/route/rule sites hook_socket.c # SO_BINDTODEVICE / SO_BINDTOIFINDEX concealment hook_fs.c # optional VFS path concealment (CONFIG_VPNHIDE_FS_HIDING) vpnhide_internal.h # brain API shared between core.c and hook_*.c Kconfig Makefile include/linux/vpnhide.h # public call-site API + CONFIG_VPNHIDE=n stubs (copied to /include/linux/) versions//*.patch # per-version call-site patches (9 KMIs: 6.1..6.12, 5.15/5.10, 5.4/4.19/4.14/4.9) scripts/integrate.py # export/apply full integration and review bundle scripts/integration_rules.py # exact insertion rules for each supported source shape ``` `integrate.py` vendors `kmod/shared/vpnhide_logic.h` and `kmod/generated/{iface_lists,hook_ids}.h` into `security/vpnhide/` so the filter logic and interface tables stay a single source of truth in the repo (generated by the existing `scripts/codegen-*.py`). ## Hook → kernel call-site map The same 11 kernel hooks the `.ko` installs (`VPNHIDE_KERNEL_HOOK_MASK`) plus the optional filesystem set. Each patch passes the hook id so per-hook masks and stats are preserved. | hook id | call site (android14-6.1) | in-tree entry | |---|---|---| | DEV_IOCTL (5) | `dev_ifsioc_locked` / `dev_ifname` | `vpnhide_should_hide_ifname` | | SOCK_IOCTL (6) | `dev_ifconf` (SIOCGIFCONF loop) | `vpnhide_should_hide_dev` | | RTNL_FILL_IFINFO (2) | `rtnl_fill_ifinfo` | `vpnhide_should_hide_dev` | | INET_FILL_IFADDR (3) | `inet_fill_ifaddr` | `vpnhide_should_hide_dev` | | INET6_FILL_IFADDR (4) | `inet6_fill_ifaddr` | `vpnhide_should_hide_dev` | | FIB_ROUTE_SEQ_SHOW (0) | `fib_route_seq_show` (/proc/net/route) | route/rule predicate (Phase 3b-ii) | | IPV6_ROUTE_SEQ_SHOW (1) | `ipv6_route_seq_show` (/proc/net/ipv6_route) | route/rule predicate (Phase 3b-ii) | | FIB_DUMP_INFO (7) | `fib_dump_info` (RTM_GETROUTE v4) | route/rule predicate (Phase 3b-ii) | | RT6_FILL_NODE (8) | `rt6_fill_node` (RTM_GETROUTE v6) | route/rule predicate (Phase 3b-ii) | | FIB_NL_FILL_RULE (9) | `fib_nl_fill_rule` (policy rules) | route/rule predicate (Phase 3b-ii) | | SOCKET_BIND_INTERFACE (25) | `__sys_setsockopt` (SO_BINDTODEVICE/IFINDEX) | `vpnhide_setsockopt_bind` | | FILESYSTEM_IFACE_PATHS (27) | `filename_lookup`/`do_filp_open`/`vfs_getattr`/`iterate_dir` | `vpnhide_should_hide_dentry` / `vpnhide_readdir_begin`/`_end` | In-tree, the trickiest `.ko` hook — SO_BINDTODEVICE — is **simpler and more correct**: the patch runs at the syscall call-site in process context *before* `sock->ops->...` mutates socket state, so the .ko's PC-redirect + fault-in-atomic dance disappears. ## Status / phases - [x] Phase 0 — protocol: `VPNHIDE_BACKEND_BUILTIN = 4` + `VPNHIDE_BUILTIN_HOOK_MASK` (codegen, append-only) - [x] Phase 1 — foundation: public header, brain (`core.c`), `vpnhide_internal.h`, Kconfig, Makefile - [x] Phase 2a — network hook bodies: `hook_iface.c` (dev/ifname predicate), `hook_socket.c` (bind) - [x] Phase 2b — `hook_fs.c` (optional VFS path concealment + readdir filtering) - [x] Phase 3a — `integrate.py` (driver copy + header/table vendoring + security wiring + patch apply) - [x] Phase 3b(i) — call-site patches for `android14-6.1`: ioctl (dev_ifname/ifsioc/ifconf), link/addr dumps (rtnl_fill_ifinfo, inet{,6}_fill_ifaddr), SO_BINDTODEVICE (__sys_setsockopt). Generated from the rules now in `scripts/integration_rules.py`; **compile-validated** against kernel/common 6.1.174 with the GKI clang (driver + all patched objects, zero warnings). - [x] Phase 3b(ii) — route/rule patches (fib_route/ipv6_route seq_show, fib_dump_info, rt6_fill_node, fib_nl_fill_rule) via hook_route.c. Uses the kernel's own fib_info_nhc()/nexthop_fib6_nh() accessors; hides VPN-iface routes, a public host-route pinned to a physical uplink (server-IP leak), and the target UID's policy rule. Compile-validated on 6.1.174, zero warnings. - [x] Phase 3b(iii) — fs call-site patches (filename_lookup, do_filp_open, vfs_getattr, iterate_dir). Compile-validated in BOTH configs: FS_HIDING=n (patched fs objects build against header stubs, no hook_fs.o) and FS_HIDING=y (real hooks + hook_fs.o), zero warnings. - [x] Phase 5a — activator: `activate_builtin` / `boot_service_builtin` / `uninstall_builtin` + `builtin` bin - [x] Phase 5b — app (Kotlin): `NativeBackendId.Builtin`, snapshot section for the ctl `backend` id (disambiguates kmod vs builtin on the shared node), `detectBuiltinModule`, dashboard card - [x] Phase 5c — `vpnhide_builtin` companion module (module.prop + boot scripts running the activator) - [x] Phase 4 — QEMU functional run gate (builtin/test/): boots an Image with CONFIG_VPNHIDE=y and runs the shared vector suite. android14-6.1: pass=35 fail=0 panic=0 — every vector hidden for the target UID, preserved for the non-target (ioctl/getifaddrs/routes/host-route/rule/fs/all bind cases). - [x] Phase 6a — packaging: `builtin/build.py` cross-compiles the `builtin` activator (cargo-ndk) and packages `builtin/module/` into `vpnhide-builtin.zip` (KMI-agnostic; no .ko). Feature is now installable end-to-end; changelog fragment added. - [x] Phase 6b (android16-6.12) — DONE. The integration rules derive it from 6.1 (26/30 anchors shared) with 4 overrides (do_sock_setsockopt bind, const-ifa addr fills, iterate_shared-only readdir). QEMU gate on 6.12.89: pass=35 fail=0 panic=0. Two KMIs proven end-to-end. - [x] Phase 6c (modern KMIs) — DONE. The integration rules derive each from 6.1 by anchor-divergence probing: `android15-6.6` (2 overrides: shares 6.12's do_sock_setsockopt + iterate_shared-only readdir), `android13-5.15` (1 override: no net/core/dev.h, include after ), `android12-5.10` (3 overrides: wext include, `int done;` in dev_ifconf's loop, putname() in filename_lookup). QEMU gate green on 6.6.139 / 5.15.208 / 5.10.257: each pass=35 fail=0 panic=0. **Five KMIs proven end-to-end** (6.1, 6.6, 6.12, 5.15, 5.10) — all post-`sockptr_t`. - [x] Phase 6c (legacy) — DONE. `android11-5.4`, `android10-4.19`, `android10-4.14`, `android10-4.9`. These are built from pinned AOSP source with Bootlin gcc 7.3 (`builtin/test/build-source-kernel.sh`) — there is no ddk-min image below 5.10 — and QEMU-booted with `-cpu cortex-a57`. Each needed driver version gates, all keyed off `LINUX_VERSION_CODE`, so the GKI KMIs are untouched: - `sockptr_t` (5.9): pre-5.9 `__sys_setsockopt` takes `char __user *optval`, so bind uses `vpnhide_setsockopt_bind_user()` and freezes by swapping optval under `set_fs(KERNEL_DS)`. - `proc_ops` (5.6) → `file_operations`; `proc_create_single` (4.18) → `single_open` fops; `static_assert` (5.1) → `_Static_assert`. - `fib_rt_info` (5.5) → `vpnhide_hide_fib_dump_raw(fi, dst, dst_len)`. - nexthop objects (5.3) → `fib_nh[0].nh_dev` / embedded `fib6_nh.nh_dev`. - `fib6_info` (4.19; 4.14 still has `rt6_info`) → the v6 route hooks switch type via `VH_FIB6_T`. QEMU gate green on 5.4.x / 4.19.x / 4.14.x / 4.9.x: each pass=35 fail=0 panic=0. 4.9 reuses 4.14's driver paths wholesale (only 4 call sites diverge: old ``, 2-arg `vfs_getattr_nosec`, rt6_fill_node's prefix block). **Nine KMIs proven end-to-end** (6.1, 6.6, 6.12, 5.15, 5.10, 5.4, 4.19, 4.14, 4.9). - [x] Phase 7 — CI gate: `builtin-qemu` job in `.github/workflows/ci.yml` runs the QEMU vector suite on the modern non-LTO KMIs (6.1 / 6.6 / 6.12) per PR — applies `integrate.py` to the baked ddk-qemu kernel tree, rebuilds the Image with `CONFIG_VPNHIDE=y`, and boots it. `run.sh` takes its native probes as prebuilt `VPNHIDE_*_BIN` (the ddk-qemu image has no NDK). All three green. Remaining follow-ups (not blockers for the backend itself): - CI coverage for the full-LTO GKI KMIs (5.10 / 5.15) and the from-source legacy KMIs, like the KPM's `kpm-qemu-legacy` job. Only 6.1 / 6.6 / 6.12 are gated today. - A publish job for `vpnhide-builtin.zip` plus `update-json/update-builtin.json`, so in-app module updates resolve (the zip builds via `builtin/build.py`, but nothing on `main` serves the update JSON yet). ### Maintaining call sites Maintain the per-KMI transformations in `scripts/integration_rules.py`, alongside source-appropriate anchors. Update the review obligations in `integrate.py` when a hook's ownership, locking or return-value requirements change. Export against the supported baselines, inspect the complete diff, then build and run the QEMU gates. The old `versions//*.patch` files are reference fixtures, not a second source to edit or an input to the new integration command. ## Python integration and review bundles The old `apply.sh` and mutating `gen_patches.py` entry points have been removed. Use Python 3.12+ through uv; runtime dependencies are Python's standard library and Git. Existing shell QEMU harnesses still orchestrate their builds, but call this Python integrator. The host needs uv (the container harness mounts its binary and lets uv provision Python 3.12). Generate a complete patch against the **actual pre-vpnhide tree**, including any existing SUSFS/ZeroMount/local modifications. The tree need not be Git-clean or even a Git checkout. Export never changes it: ```sh uv run builtin/scripts/integrate.py export --kernel /path/to/common \ --kmi android14-6.1 --output /path/to/new-review-directory ``` The bundle contains: - `vpnhide.patch`: call sites, driver, shared/generated headers and build wiring. - `report.json`: revisions, exact input/output hashes, anchor match counts, replacements, referenced symbols and checks actually performed. - `report.md`: readable edits and maintainer-written semantic review obligations. - `before/` and `after/`: full affected files, with the complete driver/header implementation in `after/` for inspecting cleanup, locking and return paths. Each edit must match exactly once. For pure include insertions only, an explicitly recorded fallback can match the unique preceding header when another layer inserted headers between the original neighbors. Executable-code anchors are never relaxed. The integrator uses the declared KMI's known source shape; it does not guess unknown API variants, parse C semantically or fall back to fuzzy matching. The complete patch is reapplied to an isolated baseline and checked against expected bytes and executable modes. Compilation and runtime tests are explicitly marked `not_run`; matching anchors is not a semantic safety verdict. Inspect calling context beyond the bundle if needed. The per-KMI files under `versions/` remain historical/reference fixtures; they are no longer an integration input. Edit `integration_rules.py` to maintain call sites, then export against each supported kernel baseline. To prepare and immediately apply the same verified transformations: ```sh uv run builtin/scripts/integrate.py apply --kernel /path/to/common \ --kmi android14-6.1 --output /path/to/another-new-review-directory ``` `apply` verifies input hashes again before writing, uses per-file atomic replacement, and restores already-written files on handled write failures. It is not a filesystem transaction: do not run another writer concurrently; process termination/power loss can leave partial output. Reapplying/upgrading an existing integration is deliberately rejected: regenerate the pre-vpnhide base instead. Output directories must be new and outside the kernel tree. For a reviewed exported patch, use `git apply --check /path/to/vpnhide.patch` then `git apply /path/to/vpnhide.patch` from the same pre-integration tree. Git's context check does not verify every input hash in the report; preserve that baseline and do not treat a patch that happens to apply elsewhere as having been reviewed there. ```sh uv run --python 3.12 --no-project python -m unittest discover \ -s builtin/scripts -p 'test_*.py' -v ```