vpnhide/kmod/Makefile
Danila Gornushko 1aa221c955
fix(kpm): make the ctl0 status/stats channel reliable on real devices (#216)
* 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.
2026-06-30 18:09:26 +03:00

100 lines
4.1 KiB
Makefile

obj-m += vpnhide_kmod.o
ifeq ($(KERNELRELEASE),)
# Default target stays the LKM (.ko) build, even though `kpm` is defined first.
.DEFAULT_GOAL := all
# clang is needed by both backends; prefer $(CLANG_DIR) if the .env set it.
CLANG := $(if $(CLANG_DIR),$(CLANG_DIR)/clang,clang)
# ================================================================== #
# KPM backend — relocatable `.kpm`, KernelPatch headers, NO kernel #
# source / Module.symvers. Build: make kpm KP_DIR=/path/KernelPatch #
# ================================================================== #
# KernelPatch header tree — vendored as a submodule by default; override
# KP_DIR to point at your own checkout. Run `git submodule update --init`.
KP_DIR ?= $(CURDIR)/third_party/KernelPatch
# Include dirs mirror KernelPatch's own kpms/*/Makefile INCLUDE_DIRS.
KPM_INCLUDE_DIRS := . include patch/include linux/include \
linux/arch/arm64/include linux/tools/arch/arm64/include
KPM_INCLUDES := $(foreach d,$(KPM_INCLUDE_DIRS),-I$(KP_DIR)/kernel/$(d))
# -fuse-ld=lld: the `-r` link targets aarch64; lld handles the cross link with
# one binary. DDK clang bundles lld and defaults to it, but a plain distro clang
# (the legacy KPM image) defaults to the host GNU ld, which can't do the aarch64
# relocatable link — force lld so both toolchains build the .kpm identically.
# -nostdlib: this is a freestanding relocatable kernel object; older Android
# clang drivers (e.g. clang-12 on android12-5.10) otherwise add CRT startup
# files + -lc/-ldl to the `-r` link and fail (crtbegin_dynamic.o / -lc not found).
# -mcmodel=large: REQUIRED, do not change to small. KernelPatch loads the .kpm
# in a vmalloc region that can sit >4 GB from where ADRP-relative literals were
# assumed, so -mcmodel=small's ADR_PREL_PG_HI21 relocations for .rodata (string
# literals) overflow and resolve to garbage addresses — the ctl0 status/stats
# replies came back corrupted on a real APatch device until this was switched to
# large (absolute MOVW_UABS materialisation, no ±4 GB range limit). The .ko is
# unaffected because Kbuild handles module addressing itself.
KPM_CFLAGS := -target aarch64-linux-android -fuse-ld=lld -nostdlib \
-O2 -fno-pic -fno-pie -fno-plt -mcmodel=large \
-fvisibility=hidden -mno-outline-atomics \
-fno-stack-protector -fno-common -fno-builtin -ffreestanding \
-fno-asynchronous-unwind-tables -fno-unwind-tables -nostdinc \
-D__KERNEL__ -DCONFIG_ARM64 -DMODNAME=\"vpnhide\"
.PHONY: all strip clean kpm kpm-clean
kpm:
@if [ ! -f "$(KP_DIR)/kernel/include/kpmodule.h" ]; then \
echo "KernelPatch headers not found at $(KP_DIR)."; \
echo "Run: git submodule update --init kmod/third_party/KernelPatch"; \
echo "(or override KP_DIR=/path/to/KernelPatch)"; \
exit 1; \
fi
$(CLANG) $(KPM_CFLAGS) $(KPM_INCLUDES) -I$(CURDIR) -r \
-o vpnhide.kpm kpm/vpnhide_kpm.c
kpm-clean:
rm -f vpnhide.kpm
# ================================================================== #
# LKM backend — `.ko` via Kbuild. Requires the GKI kernel headers. #
# These requirements must NOT fire for the `kpm` targets above. #
# ================================================================== #
ifeq ($(filter kpm kpm-clean,$(MAKECMDGOALS)),)
# KERNEL_SRC and CLANG_DIR come from the environment (set via .env + direnv).
ifndef KERNEL_SRC
$(error KERNEL_SRC is not setcopy .env.example to .env and fill in paths)
endif
ifndef CLANG_DIR
$(error CLANG_DIR is not setcopy .env.example to .env and fill in paths)
endif
ARCH := arm64
CROSS_COMPILE := aarch64-linux-gnu-
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
MAKE_ARGS := -C $(KERNEL_SRC) M=$(CURDIR) \
ARCH=$(ARCH) LLVM=1 LLVM_IAS=1 \
CROSS_COMPILE=$(CROSS_COMPILE) \
CC=$(CC) LD=$(LD) AR=$(AR) NM=$(NM) \
OBJCOPY=$(OBJCOPY) OBJDUMP=$(OBJDUMP) STRIP=$(STRIP)
all:
$(MAKE) $(MAKE_ARGS) modules
strip: all
$(STRIP) -d $(CURDIR)/vpnhide_kmod.ko
clean:
$(MAKE) $(MAKE_ARGS) clean
endif # not a kpm target
endif # KERNELRELEASE