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
8.9 KiB
Building vpnhide-kmod for a new device
This guide walks through building the kernel module for any Android device with a GKI 2.0 kernel (Android 12+, kernel 5.10+). The process was developed and tested on Pixel 8 Pro (android14-6.1) and applies identically to any GKI generation — only the kernel branch and Module.symvers change.
Total time: ~15 minutes once toolchain is set up. Download size: ~500 MB (shallow kernel clone) + ~100 MB (toolchain if not already available).
Prerequisites
- A rooted Android device with KernelSU or Magisk (for adb root shell)
- Linux host (Arch, Ubuntu, Debian — anything with make, clang, git)
adbconnected to the devicemodprobeon the host (for extracting CRCs from .ko files)direnv(for automatic env var loading)
Step 1: Identify the GKI generation
adb shell uname -r
Example outputs and what they mean:
uname -r output |
GKI generation | Kernel branch |
|---|---|---|
5.10.xxx-android13-4-g... |
android13-5.10 | android13-5.10 |
5.15.xxx-android14-6-g... |
android14-5.15 | android14-5.15 |
6.1.xxx-android14-11-g... |
android14-6.1 | android14-6.1 |
6.1.xxx-android15-8-g... |
android15-6.1 | android15-6.1 |
6.6.xxx-android15-... |
android15-6.6 | android15-6.6 |
The androidXX part is the GKI generation (kernel branch name), NOT
the Android version running on the device. A Pixel 7 Pro running
Android 16 still has an android13-5.10 kernel because the generation
is frozen at manufacturing time.
Step 2: Clone the full Pixel kernel tree
Google ships a single android14-6.1 kernel covering all Pixels from 6 to
9a. The full tree includes kernel sources, prebuilt toolchain, device trees,
and all out-of-tree Google modules — everything needed with no extra downloads:
mkdir ~/pixel-kernel && cd ~/pixel-kernel
repo init --depth=1 \
-u https://android.googlesource.com/kernel/manifest \
-b android-gs-shusky-6.1-android16
repo sync -c --no-tags -j$(nproc)
Then build once to produce all artifacts:
./build_shusky.sh # or build_raviole.sh etc. — same kernel image for all
Set an env var pointing to the tree root — subsequent steps reference it:
export PIXEL_KERNEL_TREE=~/pixel-kernel
Useful paths within the tree
| Path | Purpose |
|---|---|
$PIXEL_KERNEL_TREE/aosp/ |
Full GKI kernel source tree — browse subsystems, understand exported APIs, read in-tree driver implementations |
$PIXEL_KERNEL_TREE/out/shusky/dist/kernel-headers.tar.gz |
Kernel headers for building against (extract and use as KDIR) |
$PIXEL_KERNEL_TREE/out/shusky/dist/vmlinux.symvers |
Symbol versions for modpost — use instead of extracting CRCs from device .ko files |
$PIXEL_KERNEL_TREE/out/shusky/dist/System.map |
Symbol addresses for debugging |
$PIXEL_KERNEL_TREE/private/google-modules/ |
Reference implementations of out-of-tree modules (Kconfig, Makefile patterns) |
$PIXEL_KERNEL_TREE/prebuilts/clang/host/linux-x86/clang-r487747c/bin |
Bundled clang 17.0.2 — exact compiler used to build the kernel |
raviole/dist/ and shusky/dist/ contain identical vmlinux.symvers and
Image since it's the same kernel build — use either.
Prepare kernel headers for module builds
mkdir -p ~/pixel-kernel-headers
tar -xzf $PIXEL_KERNEL_TREE/out/shusky/dist/kernel-headers.tar.gz \
-C ~/pixel-kernel-headers
cp $PIXEL_KERNEL_TREE/out/shusky/dist/vmlinux.symvers \
~/pixel-kernel-headers/Module.symvers
Then skip to step 3.
Alternative: shallow clone (non-Pixel or different GKI generation)
If you don't need the full tree, clone just the kernel source:
BRANCH="android13-5.10" # ← replace with your generation from step 1
git clone --depth=1 -b $BRANCH \
https://android.googlesource.com/kernel/common \
~/kernel-source
You'll then need to prepare it manually — see Preparing a standalone kernel source at the end.
Step 3: Configure .env
The build system uses direnv to load KERNEL_SRC and CLANG_DIR from
a .env file. Copy the example and fill in your paths:
cd kmod/
cp .env.example .env
Edit .env:
# Pixel kernel tree approach (after extracting headers above):
KERNEL_SRC=~/pixel-kernel-headers
CLANG_DIR=~/pixel-kernel/prebuilts/clang/host/linux-x86/clang-r487747c/bin
# Or standalone kernel source approach:
# KERNEL_SRC=~/kernel-source
# CLANG_DIR=/path/to/clang/bin
Allow direnv to load it:
direnv allow
From now on, entering the kmod/ directory automatically exports
KERNEL_SRC and CLANG_DIR. The Makefile reads both from the environment.
Step 4: Build and package
make # builds vpnhide_kmod.ko
./build-zip.sh # builds .ko (if needed) + packages vpnhide-kmod.zip
Step 5: Install and test
adb push vpnhide-kmod.zip /sdcard/Download/
# Install via KernelSU-Next manager -> Modules -> Install from storage
# Reboot
After reboot, verify:
# Module loaded?
adb shell "su -c 'lsmod | grep vpnhide'"
# kretprobes registered?
adb shell "su -c 'dmesg | grep vpnhide'"
# UIDs loaded?
adb shell "su -c 'cat /proc/vpnhide_targets'"
Pick target apps via the WebUI in KernelSU-Next manager.
Preparing a standalone kernel source
If you used the shallow clone (non-Pixel path), you need to prepare the kernel source before building. If you used the Pixel kernel tree with extracted headers, skip this section entirely.
Pull .config from the device
adb shell "su -c 'gzip -d < /proc/config.gz'" > ~/kernel-source/.config
If /proc/config.gz doesn't exist, use the GKI defconfig:
cd ~/kernel-source
make ARCH=arm64 LLVM=1 CC=$CLANG_DIR/clang gki_defconfig
Generate Module.symvers from device .ko files
The Module.symvers file contains CRC checksums for every exported kernel symbol. These must match the running kernel exactly (CONFIG_MODVERSIONS):
# Pull all vendor modules from the device
mkdir -p /tmp/device-modules
adb shell "su -c 'ls /vendor/lib/modules/*.ko'" | tr -d '\r' | while read ko; do
adb shell "su -c 'cat $ko'" > "/tmp/device-modules/$(basename $ko)"
done
# Extract CRCs from all modules and build Module.symvers
for ko in /tmp/device-modules/*.ko; do
modprobe --dump-modversions "$ko" 2>/dev/null
done | sort -u -k2 | \
awk '{printf "%s\t%s\tvmlinux\tEXPORT_SYMBOL\t\n", $1, $2}' \
> ~/kernel-source/Module.symvers
echo "Generated Module.symvers with $(wc -l < ~/kernel-source/Module.symvers) symbols"
Expect 3000-5000 symbols. If you get 0, check that modprobe is
installed (apt install kmod or pacman -S kmod).
Alternative: if the device's ROM has a -kernels repo on GitHub
(e.g. crdroidandroid/android_device_google_shusky-kernels), you can
download the .ko files from there instead of pulling from device.
Prepare headers
cd ~/kernel-source
# Create empty ABI symbol list (GKI build expects it)
touch abi_symbollist.raw
# Generate headers
make ARCH=arm64 LLVM=1 LLVM_IAS=1 \
CC=$CLANG_DIR/clang LD=$CLANG_DIR/ld.lld AR=$CLANG_DIR/llvm-ar \
NM=$CLANG_DIR/llvm-nm OBJCOPY=$CLANG_DIR/llvm-objcopy \
OBJDUMP=$CLANG_DIR/llvm-objdump STRIP=$CLANG_DIR/llvm-strip \
CROSS_COMPILE=aarch64-linux-gnu- \
olddefconfig prepare
Common issue: make prepare may fail on tools/bpf/resolve_btfids
due to host clang version mismatch. This is fine — the module can
still build without BTF. Ignore this error.
Set UTS_RELEASE (vermagic)
KernelSU bypasses vermagic checks, so any value works if using KSU.
For Magisk or manual insmod, the vermagic must match uname -r.
cd ~/kernel-source
# For KernelSU users (any placeholder works):
echo '#define UTS_RELEASE "6.1.0-vpnhide"' > include/generated/utsrelease.h
echo -n "6.1.0-vpnhide" > include/config/kernel.release
# For Magisk users (must match exactly):
KVER="$(adb shell uname -r | tr -d '\r')"
echo "#define UTS_RELEASE \"$KVER\"" > include/generated/utsrelease.h
echo -n "$KVER" > include/config/kernel.release
Then set KERNEL_SRC=~/kernel-source in your .env and proceed to step 4.
Troubleshooting
insmod: Exec format error
- Vermagic mismatch (Magisk doesn't bypass it). Set UTS_RELEASE to
exact
uname -rvalue. - Or Module.symvers CRCs don't match — re-extract from device .ko files.
insmod: File exists
- Module already loaded.
rmmod vpnhide_kmodfirst.
modprobe --dump-modversions: no output
- .ko files might be stripped. Try pulling from a different path
(
/vendor/lib/modules/,/system/lib/modules/,/lib/modules/$(uname -r)/).
make prepare fails on resolve_btfids
- Ignore — BTF is optional. The module builds without it.
No /proc/config.gz
- Use
make gki_defconfiginstead.
kretprobe not firing (ioctl not filtered)
- Check
dmesg | grep vpnhidefor registration messages. - Check
/proc/vpnhide_targetshas the right UIDs. - The target app's UID changes on reinstall — re-resolve via WebUI.