From 948d07d9ca28c0d79222e61c6f216850e6f8c86b Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 02:59:56 +0800 Subject: [PATCH 01/35] fix(kpm): validate ELF section bounds and guard module query callbacks --- kernel/patch/module/module.c | 82 +++++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/kernel/patch/module/module.c b/kernel/patch/module/module.c index 5728dde..e852cd5 100644 --- a/kernel/patch/module/module.c +++ b/kernel/patch/module/module.c @@ -464,12 +464,48 @@ static int setup_load_info(struct load_info *info) static int elf_header_check(struct load_info *info) { - if (info->len <= sizeof(*(info->hdr))) return -ENOEXEC; - if (memcmp(info->hdr->e_ident, ELFMAG, SELFMAG) || info->hdr->e_type != ET_REL || !elf_check_arch(info->hdr) || - info->hdr->e_shentsize != sizeof(Elf_Shdr)) + if (!info || !info->hdr || info->len < (int)sizeof(Elf_Ehdr)) return -ENOEXEC; + + const Elf_Ehdr *hdr = info->hdr; + unsigned long len = (unsigned long)info->len; + if (memcmp(hdr->e_ident, ELFMAG, SELFMAG) || + hdr->e_ident[EI_CLASS] != ELFCLASS64 || + hdr->e_ident[EI_DATA] != ELFDATA2LSB || + hdr->e_type != ET_REL || !elf_check_arch(hdr) || + hdr->e_shentsize != sizeof(Elf_Shdr) || !hdr->e_shnum || + hdr->e_shstrndx == SHN_UNDEF || hdr->e_shstrndx >= hdr->e_shnum) return -ENOEXEC; - if (info->hdr->e_shoff >= info->len || (info->hdr->e_shnum * sizeof(Elf_Shdr) > info->len - info->hdr->e_shoff)) + + /* Check bounds before setup_load_info dereferences the section table. */ + if (hdr->e_shoff > len || + hdr->e_shnum > (len - hdr->e_shoff) / sizeof(Elf_Shdr)) + return -ENOEXEC; + + const Elf_Shdr *sections = (const void *)((const char *)hdr + hdr->e_shoff); + const Elf_Shdr *names = §ions[hdr->e_shstrndx]; + if (names->sh_type != SHT_STRTAB || !names->sh_size || + names->sh_offset > len || names->sh_size > len - names->sh_offset) return -ENOEXEC; + + const char *shstrtab = (const char *)hdr + names->sh_offset; + for (unsigned int i = 0; i < hdr->e_shnum; ++i) { + const Elf_Shdr *sec = §ions[i]; + if (sec->sh_type != SHT_NOBITS && + (sec->sh_offset > len || sec->sh_size > len - sec->sh_offset)) + return -ENOEXEC; + if (sec->sh_name >= names->sh_size || + !memchr(shstrtab + sec->sh_name, '\0', names->sh_size - sec->sh_name)) + return -ENOEXEC; + if (sec->sh_type == SHT_SYMTAB && + (sec->sh_link >= hdr->e_shnum || + sections[sec->sh_link].sh_type != SHT_STRTAB || + sec->sh_entsize != sizeof(Elf_Sym) || + (sec->sh_size % sizeof(Elf_Sym)) != 0)) + return -ENOEXEC; + if ((sec->sh_type == SHT_REL || sec->sh_type == SHT_RELA) && + (sec->sh_info >= hdr->e_shnum || sec->sh_link >= hdr->e_shnum)) + return -ENOEXEC; + } return 0; } @@ -529,6 +565,12 @@ long load_module(const void *data, int len, const char *args, const char *event, /* Mark this KPM as loading (for crash detection) */ kpm_safety_mark_loading(info->info.name); + /* init is mandatory; an exit callback is optional. Never jump to NULL. */ + if (!mod->init || !*mod->init) { + rc = -ENOEXEC; + logkfe("KPM has no callable init entry point\n"); + goto free; + } rc = (*mod->init)(mod->args, event, reserved); if (!rc) { @@ -537,7 +579,8 @@ long load_module(const void *data, int len, const char *args, const char *event, goto out; } else { logkfi("[%s] failed with [%s] error: %d, try exit ...\n", mod->info.name, args, rc); - (*mod->exit)(reserved); + if (mod->exit && *mod->exit) + (*mod->exit)(reserved); } free: @@ -718,15 +761,28 @@ int get_module_nums() int list_modules(char *out_names, int size) { + if (!out_names || size <= 0) return -EINVAL; + out_names[0] = '\0'; rcu_read_lock(); struct module *pos; int off = 0; list_for_each_entry(pos, &modules.list, list) { - off += snprintf(out_names + off, size - 1 - off, "%s\n", pos->info.name); + int remaining = size - off; + if (remaining <= 1) break; + int n = snprintf(out_names + off, remaining, "%s\n", pos->info.name); + if (n < 0) { + off = n; + break; + } + if (n >= remaining) { + off = size - 1; + break; + } + off += n; } - if (off > 0) out_names[off - 1] = '\0'; + if (off > 0 && out_names[off - 1] == '\n') out_names[off - 1] = '\0'; rcu_read_unlock(); return off; @@ -734,13 +790,16 @@ int list_modules(char *out_names, int size) int get_module_info(const char *name, char *out_info, int size) { - if (size <= 0) return 0; + if (!name || !out_info || size <= 0) return -EINVAL; rcu_read_lock(); struct module *mod = find_module(name); - if (!mod) return -ENOENT; + if (!mod) { + rcu_read_unlock(); + return -ENOENT; + } - int sz = snprintf(out_info, size - 1, + int sz = snprintf(out_info, size, "name=%s\n" "version=%s\n" "license=%s\n" @@ -754,7 +813,8 @@ int get_module_info(const char *name, char *out_info, int size) mod->info.description ?: "", mod->args ?: ""); - if (sz > 0) out_info[sz - 1] = '\0'; + if (sz >= size) sz = size - 1; + if (sz > 0 && out_info[sz - 1] == '\n') out_info[sz - 1] = '\0'; logkfd("%s", out_info); rcu_read_unlock(); From 01223df8f2b0463afc61945b2d9ea14f65cbddea Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:00:46 +0800 Subject: [PATCH 02/35] feat(runtime): define shared versioned management ABI for both backends --- include/patchnest/runtime_abi.h | 91 +++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 include/patchnest/runtime_abi.h diff --git a/include/patchnest/runtime_abi.h b/include/patchnest/runtime_abi.h new file mode 100644 index 0000000..1f1f37e --- /dev/null +++ b/include/patchnest/runtime_abi.h @@ -0,0 +1,91 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * PatchNest dual-backend management ABI, version 1. + * + * This is a wire contract shared by an in-tree kernel implementation, + * an image-patched kpimg implementation, and their management clients. + * Declaring the ABI does NOT mean an implementation is already connected. + * + * Never pass raw user pointers, kernel addresses, or function pointers + * through this structure. Every implementation must authenticate each + * mutating operation, validate size/version, and copy bounded payloads. + */ +#ifndef PATCHNEST_RUNTIME_ABI_H +#define PATCHNEST_RUNTIME_ABI_H + +#ifdef __KERNEL__ +#include +typedef __u32 pn_u32; +typedef __u64 pn_u64; +#else +#include +typedef uint32_t pn_u32; +typedef uint64_t pn_u64; +#endif + +#define PATCHNEST_ABI_MAJOR 1u +#define PATCHNEST_ABI_MINOR 0u +#define PATCHNEST_ABI_VERSION ((PATCHNEST_ABI_MAJOR << 16) | PATCHNEST_ABI_MINOR) +#define PATCHNEST_KPM_NAME_MAX 64u + +/* Backends are mutually exclusive for an active kernel session. */ +enum patchnest_backend { + PATCHNEST_BACKEND_NONE = 0, + PATCHNEST_BACKEND_BUILTIN = 1, /* compiled into kernel, distributed in AK3 */ + PATCHNEST_BACKEND_PATCHED = 2, /* freestanding kpimg, offline image patch */ +}; + +/* Advertised capabilities, never infer support from backend type alone. */ +#define PATCHNEST_CAP_KPM_LIST (1u << 0) +#define PATCHNEST_CAP_KPM_LOAD (1u << 1) +#define PATCHNEST_CAP_KPM_UNLOAD (1u << 2) +#define PATCHNEST_CAP_KPM_EVENT (1u << 3) +#define PATCHNEST_CAP_EMBEDDED (1u << 4) +#define PATCHNEST_CAP_SAFE_MODE (1u << 5) +#define PATCHNEST_CAP_AUDIT_LOG (1u << 6) + +enum patchnest_safety_state { + PATCHNEST_SAFETY_NOT_READY = 0, + PATCHNEST_SAFETY_READY = 1, + PATCHNEST_SAFETY_QUARANTINED = 2, + PATCHNEST_SAFETY_SAFE_MODE = 3 +}; + +/* + * First-stage readonly handshake. Client initializes struct_size to + * sizeof(struct patchnest_status_v1). Providers must reject smaller + * structures and zero fields they do not support. Integers are native + * endian, little-endian on supported Android arm64 targets. + */ +struct patchnest_status_v1 { + pn_u32 struct_size; + pn_u32 abi_version; + pn_u32 backend; + pn_u32 capabilities; + pn_u32 safety_state; + pn_u32 loaded_kpm_count; + pn_u64 boot_session_id; +}; + +enum patchnest_operation { + PATCHNEST_OP_STATUS = 1, + PATCHNEST_OP_LIST_KPM = 2, + PATCHNEST_OP_LOAD_KPM = 3, + PATCHNEST_OP_UNLOAD_KPM = 4, + PATCHNEST_OP_DISPATCH_EVENT = 5, + PATCHNEST_OP_READ_AUDIT = 6, +}; + +/* + * All requests have this prefix. size includes the complete payload. + * No operation may bypass caller authentication or safe-mode gating. + * Runtime implementations set strict maximum payload lengths per op. + */ +struct patchnest_request_v1 { + pn_u32 size; + pn_u32 abi_version; + pn_u32 operation; + pn_u32 flags; /* MUST be zero in v1 */ +}; + +#endif /* PATCHNEST_RUNTIME_ABI_H */ From e6acf62cb9105c6ae69910256fc25a2d0177a8c3 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:01:02 +0800 Subject: [PATCH 03/35] test(runtime): assert stable v1 ABI layout and backend capability semantics --- tests/patchnest_abi_smoke.c | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/patchnest_abi_smoke.c diff --git a/tests/patchnest_abi_smoke.c b/tests/patchnest_abi_smoke.c new file mode 100644 index 0000000..8ef2ca2 --- /dev/null +++ b/tests/patchnest_abi_smoke.c @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Host-side layout and protocol smoke test for the dual-backend ABI. */ +#include +#include +#include +#include "../include/patchnest/runtime_abi.h" + +_Static_assert(sizeof(struct patchnest_request_v1) == 16, + "request header layout changed"); +_Static_assert(sizeof(struct patchnest_status_v1) == 32, + "status layout changed"); +_Static_assert(offsetof(struct patchnest_status_v1, boot_session_id) == 24, + "status field offset changed"); + +int main(void) +{ + struct patchnest_status_v1 status = { + .struct_size = sizeof(struct patchnest_status_v1), + .abi_version = PATCHNEST_ABI_VERSION, + .backend = PATCHNEST_BACKEND_PATCHED, + .capabilities = PATCHNEST_CAP_KPM_LIST | PATCHNEST_CAP_SAFE_MODE, + .safety_state = PATCHNEST_SAFETY_NOT_READY, + .loaded_kpm_count = 0, + .boot_session_id = 0, + }; + struct patchnest_request_v1 request = { + .size = sizeof(struct patchnest_request_v1), + .abi_version = PATCHNEST_ABI_VERSION, + .operation = PATCHNEST_OP_STATUS, + .flags = 0, + }; + assert(status.struct_size == 32); + assert(PATCHNEST_ABI_MAJOR == 1); + assert(PATCHNEST_BACKEND_BUILTIN != PATCHNEST_BACKEND_PATCHED); + assert((status.capabilities & PATCHNEST_CAP_KPM_LOAD) == 0); + assert(request.size == 16); + assert(request.operation == PATCHNEST_OP_STATUS); + puts("PatchNest management ABI v1: host layout PASS"); + return 0; +} From 35e756f5dfef0b14f9d0a93d4892c3a841d1379a Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:02:13 +0800 Subject: [PATCH 04/35] docs(runtime): specify dual-backend architecture, safety invariants and acceptance gates --- docs/PATCHNEST_DUAL_BACKEND.md | 104 +++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 docs/PATCHNEST_DUAL_BACKEND.md diff --git a/docs/PATCHNEST_DUAL_BACKEND.md b/docs/PATCHNEST_DUAL_BACKEND.md new file mode 100644 index 0000000..1bce289 --- /dev/null +++ b/docs/PATCHNEST_DUAL_BACKEND.md @@ -0,0 +1,104 @@ +# PatchNest Core:双后端底层重构(RFC v1) + +状态:**研发中 / 不可刷入 / 未完成真机验收** +基线:`Zhanfg/KernelPatch-Public@0bdf0094` +工作分支:`refactor/dual-backend-runtime` +核心独立仓:`Zhanfg/KernelPatch-Public` +管理端仓:`Zhanfg/PatchNest`(`cli/` + `module/` + `kpms/`) + +## 1. 必须实现的两个版本 + +| 项目 | A:Kernel Integrated | B:Image Patched | +|---|---|---| +| 核心来源 | PatchNest Core | 同一 PatchNest Core | +| 安装过程 | 将内核源码实现以 `CONFIG_PATCHNEST_CORE=y` 编译进内核,AK3 只分发该内核 | `kptools` 对受支持的内核镜像进行离线修补,把 freestanding runtime 的 `kpimg` 注入镜像 | +| 内核编译要求 | 必须有完整可编译源码与适配补丁 | 不要求重新编译目标内核,但需要兼容的镜像结构、ARM64 指令/KCFI 约束及符号解析 | +| 运行时承载 | 内核内建组件;不依赖 WebUI 或用户态守护进程存活 | 注入后的镜像内 runtime;不依赖 WebUI 或用户态守护进程存活 | +| KPM 管理 | 轻量 CLI / WebUI 调控制接口 | 同一 CLI / WebUI,动态选择 patched adapter | +| 更新内核 | 重新编译并打包 AK3 | 对新基线重测、重新修补 | +| 无法兼容 | 构建失败,不得伪装成成功 | 直接拒绝修补,不得强行写入 | +| 与其他 root 系统 | 显式能力协商,避免双重 su/hook | 显式能力协商,避免双重 su/hook | + +**澄清**:AK3 是镜像打包与安装工具,不能替代内核编译阶段的链接。把现有 `kpimg` 复制进 AK3 ZIP 不等于方案 A。 + +## 2. 分层设计 + +```text +PatchNest WebUI / CLI <- 非常薄的控制面,无需常驻 + | + protocol v1 <- include/patchnest/runtime_abi.h + | + authenticated adapter + / \ + kernel-native image-patched <- 后端特有初始化 / memory / hooks + \ / + KPM runtime <- ELF/relocations, lifecycle, events, safety + | + loaded KPMs +``` + +管理端先查询 `STATUS`,验证 ABI 主版本、`backend` 和真实能力位,**绝不通过「检测是否 root」推断内核支持 KPM**。后续管理操作:LIST、LOAD、UNLOAD、EVENT、READ_AUDIT。v1 禁止未定义 `flags`,请求必须校验大小上限、操作权限和字段边界。 + +### 核心内部接口(迁移目标) + +1. `pn_elf_validate()`:严格长度/节区/符号/重定位校验,验证在任何解引用之前执行。 +2. `pn_module_prepare()`:核验元数据与签名、准备内存权限、resolve 符号,但不发布模块。 +3. `pn_module_activate()`:受控 init,成功后才进入公开模块表。 +4. `pn_module_deactivate()`:先标记卸载并阻止新引用,等待进行中的控制/事件回调退出,再执行 exit、回收代码和数据。 +5. `pn_event_dispatch()`:可拒绝已经卸载/隔离的模块;回调不持有不允许睡眠的锁。 +6. `pn_safety_boot_guard()`:崩溃计数、安全模式、最后一次加载事务持久化;嵌入模块必须可隔离。 +7. `pn_audit()`:记录启动来源、验证结果、模块生命周期与失败原因,必要时脱敏。 + +本阶段先修复现有 loader 的独立可确认错误;**上述新函数名是目标接口,不代表都已实现**。 + +### Kernel Integrated(A)的实现路径 + +- 引入 `kernel/patchnest/Kconfig` 与 `kernel/patchnest/Makefile`,支持 arm64 的 `bool` 型 built-in 构建;**不把当前 freestanding `kernel/base/*.S` 直接作为内核 `obj-y`**。它们预设了不同的链接、地址映射、符号解析与初始化时机。 +- 创建 Linux native port:使用内核分配器、workqueue/mutex/RCU、内核已支持的安全内存权限与适用的 symbol API;必要的 hook 技术要按 GKI/CFI 版本分级,不允许全局关掉 KCFI 来凑兼容。 +- 将 KPM 装载器从 root/hiding 逻辑里分离:KPM 能力默认独立于 SU;复用现有 ReSukiSU / KernelSU 时不得安装第二个 superuser/hook 栈。 +- 模块嵌入采用 **构建期只读清单**:每个 KPM 有名称、大小、哈希、签名、加载阶段和隔离策略。嵌入更新需要重新编译或提供受信的后续部署路径;WebUI 不得声称能「运行时改写已编译内核中的只读 KPM」。 +- 给 AK3 生成设备/构建身份清单,校验对应的 `Image`、DTB/DTBO、AVB、槽位和内核 ABI。出现不匹配直接停止安装。 + +### Image Patched(B)的实现路径 + +- 保持原有 `kpimg`、`kptools` 为兼容基础;在既有能力上引入 `patched adapter`,而非立即重写所有 ARM64 重定位/指令注入逻辑。 +- 支持离线预检、原镜像备份、原子生成目标镜像、写入后比对、可回滚恢复。拒绝未知 ELF/boot header/KCFI 组合。 +- 加载 KPM 时调用相同的安全策略和生命周期算法;静态嵌入与在线加载共享同一判定。 +- 独立修补版应输出镜像或待刷入 ZIP;不直接对用户当前 boot 分区做隐式写入。 + +## 3. 统一安全规则 + +- 默认不启用未知的 KPM,嵌入包的信任链必须在安装前验证;签名策略不能仅依赖 WebUI。 +- 所有修改操作都需真实调用者授权检查;**已授权普通 UID 不等于拥有全局策略/授予其他 UID 的权限**。 +- 严禁在卸载 KPM 时释放仍被回调执行的代码;正确的引用保护/睡眠同步完成前,动态卸载不可被标记为稳定。 +- 内核安全模式能禁止所有第三方 KPM,不得与系统其他 root 管理器相互打架。 +- 不允许把 `.ko` 当作 `.kpm` 无条件兼容:必须分别验证重定位、导出符号、KCFI、许可证和生命周期。 +- 不允许修补 `init_boot` 与 `boot` 的规则互换;A/B slots、AVB 与设备差异需单独处理。 +- 运行时 idle 路径必须事件驱动,不增设每秒轮询的常驻用户态 daemon。 + +## 4. 阻塞与真实验收 + +已有真实负面测试:OnePlus 6 的 BakaSU v0.2 支持加载/嵌入指定 KPM,而 v0.3 加载/嵌入后会重启或掉入 recovery。**这不能简单归因于 PatchNest,也不能跳过回归测试。** + +现有待修 P0: #6(授权升级)、#7(superkey 长度)、#8(列表信息泄漏)、#9(TOCTOU)、#10(KPM UAF)、#11(cred/ seccomp)、#12(回调 NULL)、#13(ELF)、#14(启动计数)、#15(RCU)。这些应在正式刷机前全部审计闭环。 + +验收门槛: + +1. 代码级单元测试 + 负样本测试、UBSAN/KASAN(支持的测试内核)、静态检查。 +2. A:至少一种实际 arm64 内核 Kbuild 编译和 AK3 打包成功,验证 KPM 加载/卸载、嵌入与重启。 +3. B:原始镜像→修补→启动→KPM→重启→卸载/撤回→恢复原镜像,验证 A/B 两个槽位。 +4. 两种后端测试一致的 ABI、错误码、安全模式和事件生命周期。 +5. 真机分别执行正常场景、空指针/畸形 ELF、并发 unload/control、失败注入和已知 OP6 v0.3 回归。 + +## 5. 当前分支完成范围 + +- 已建立双后端的 **ABI v1 数据结构/能力标识**,不等于已经挂接到内核控制通道。 +- 已修正现有 KPM loader 的几类独立风险:部分 ELF 边界、init/exit 空回调、模块枚举缓冲区、未知模块查询的 RCU 锁泄漏。 +- 后续必须补齐:KPM 高并发引用/卸载协议,native port,两个实际 adapter,用户态 API 调用、Kbuild/AK3 构建和真机验证。 +- **禁止发布可刷入稳定包或自动合并 `main`,直到这些条件完成。** + +## 6. 版本与仓库治理 + +- Core 改动仅在本仓库 feature 分支;WebUI/CLI 改动将进入 `Zhanfg/PatchNest` 的对应 feature 分支。 +- 不物理迁仓、不重命名、不改变默认分支、不删除历史 Release。 +- 功能分支通过 Draft PR 审查;稳定版等真正 device gate 通过再考虑合并。 From 94362b3e8cbbfb62343d4eb45eca5bb0f210da11 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:02:26 +0800 Subject: [PATCH 05/35] ci(runtime): add fast host ABI smoke checks for dual-backend protocol --- .github/workflows/patchnest-abi.yml | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/patchnest-abi.yml diff --git a/.github/workflows/patchnest-abi.yml b/.github/workflows/patchnest-abi.yml new file mode 100644 index 0000000..cf2a48b --- /dev/null +++ b/.github/workflows/patchnest-abi.yml @@ -0,0 +1,30 @@ +name: PatchNest dual-backend ABI smoke + +on: + push: + branches: + - 'refactor/dual-backend-runtime' + pull_request: + paths: + - 'include/patchnest/**' + - 'tests/patchnest_abi_smoke.c' + - '.github/workflows/patchnest-abi.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + abi-smoke: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 + - name: Compile and test ABI contract with GCC and Clang + shell: bash + run: | + set -euo pipefail + for cc in gcc clang; do + "$cc" -std=c11 -O2 -Wall -Wextra -Werror -pedantic \ + tests/patchnest_abi_smoke.c -o /tmp/patchnest-abi-"$cc" + /tmp/patchnest-abi-"$cc" + done From d1aa176b5bc57e6581a081c2a9c180673e1360f6 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:04:57 +0800 Subject: [PATCH 06/35] feat(runtime): assign native status ioctl for ABI negotiation --- include/patchnest/runtime_abi.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/include/patchnest/runtime_abi.h b/include/patchnest/runtime_abi.h index 1f1f37e..f252bb0 100644 --- a/include/patchnest/runtime_abi.h +++ b/include/patchnest/runtime_abi.h @@ -67,6 +67,15 @@ struct patchnest_status_v1 { pn_u64 boot_session_id; }; +/* + * Kernel-native transport: STATUS is read/write so the caller supplies + * struct_size and abi_version for validation before any data is copied out. + * The patched backend is free to use the legacy supercall transport while + * preserving the same payload layout. + */ +#include +#define PATCHNEST_IOCTL_STATUS _IOWR('P', 0x01, struct patchnest_status_v1) + enum patchnest_operation { PATCHNEST_OP_STATUS = 1, PATCHNEST_OP_LIST_KPM = 2, From 4588ec0de3818781112ff691d36d750a61dd161f Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:05:26 +0800 Subject: [PATCH 07/35] feat(runtime): add kernel-native status-only adapter (Kconfig) --- ports/linux-native/Kconfig | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 ports/linux-native/Kconfig diff --git a/ports/linux-native/Kconfig b/ports/linux-native/Kconfig new file mode 100644 index 0000000..15b6e75 --- /dev/null +++ b/ports/linux-native/Kconfig @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +config PATCHNEST_CORE + bool "PatchNest built-in KPM runtime (development adapter)" + depends on ARM64 && MISC_DEVICES && KALLSYMS + default n + help + Exposes the read-only PatchNest ABI v1 status handshake at + /dev/patchnest. This development adapter does NOT yet load KPMs, + hook kernel functions, or implement embedded module management. + Keep disabled in production device configurations. From fee5418be0b623e0361abed3c28f9d47aadd419e Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:05:28 +0800 Subject: [PATCH 08/35] feat(runtime): add kernel-native status-only adapter (Makefile) --- ports/linux-native/Makefile | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 ports/linux-native/Makefile diff --git a/ports/linux-native/Makefile b/ports/linux-native/Makefile new file mode 100644 index 0000000..40e39a0 --- /dev/null +++ b/ports/linux-native/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +obj-$(CONFIG_PATCHNEST_CORE) += patchnest_native.o From f6081023c918988d87adb36b5af9ceed4c445088 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:05:31 +0800 Subject: [PATCH 09/35] feat(runtime): add kernel-native status-only adapter (patchnest_native.c) --- ports/linux-native/patchnest_native.c | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 ports/linux-native/patchnest_native.c diff --git a/ports/linux-native/patchnest_native.c b/ports/linux-native/patchnest_native.c new file mode 100644 index 0000000..86c4757 --- /dev/null +++ b/ports/linux-native/patchnest_native.c @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * PatchNest in-tree Linux adapter, bring-up stage. + * + * This device currently reports native-backend presence and zero runtime + * capabilities. It must not claim that KPM loading is operational. + * + * Integration: copy ports/linux-native into drivers/patchnest, copy + * include/patchnest into include/patchnest, source its Kconfig and + * add the directory to drivers/Makefile. + */ +#include +#include +#include +#include +#include +#include +#include "../../include/patchnest/runtime_abi.h" + +static long patchnest_native_ioctl(struct file *file, + unsigned int cmd, unsigned long arg) +{ + struct patchnest_status_v1 status; + void __user *user = (void __user *)arg; + + (void)file; + if (cmd != PATCHNEST_IOCTL_STATUS) + return -ENOTTY; + if (!user || copy_from_user(&status, user, sizeof(status))) + return -EFAULT; + + if (status.struct_size != sizeof(status) || + (status.abi_version >> 16) != PATCHNEST_ABI_MAJOR) + return -EPROTONOSUPPORT; + if (status.abi_version & 0xffffu) + return -EPROTONOSUPPORT; + + /* Fail closed: advertise no KPM capability until shared runtime works. */ + memset(&status, 0, sizeof(status)); + status.struct_size = sizeof(status); + status.abi_version = PATCHNEST_ABI_VERSION; + status.backend = PATCHNEST_BACKEND_BUILTIN; + status.capabilities = 0; + status.safety_state = PATCHNEST_SAFETY_NOT_READY; + + if (copy_to_user(user, &status, sizeof(status))) + return -EFAULT; + return 0; +} + +static const struct file_operations patchnest_native_fops = { + .owner = THIS_MODULE, + .unlocked_ioctl = patchnest_native_ioctl, +#ifdef CONFIG_COMPAT + .compat_ioctl = patchnest_native_ioctl, +#endif + .llseek = no_llseek, +}; + +static struct miscdevice patchnest_native_device = { + .minor = MISC_DYNAMIC_MINOR, + .name = "patchnest", + .fops = &patchnest_native_fops, + .mode = 0600, +}; + +static int __init patchnest_native_init(void) +{ + return misc_register(&patchnest_native_device); +} +device_initcall(patchnest_native_init); From 13de7ac84d718b555c6f375fe1a9377f722a45d9 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:05:43 +0800 Subject: [PATCH 10/35] fix(native): include kernel module and memory interfaces explicitly --- ports/linux-native/patchnest_native.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ports/linux-native/patchnest_native.c b/ports/linux-native/patchnest_native.c index 86c4757..d5bdff7 100644 --- a/ports/linux-native/patchnest_native.c +++ b/ports/linux-native/patchnest_native.c @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include "../../include/patchnest/runtime_abi.h" From 2c0ebcca2d2b92234452e7a12c6e497c3f9dacbe Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:06:22 +0800 Subject: [PATCH 11/35] fix(kpm): reject missing init before recording a load transaction --- kernel/patch/module/module.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kernel/patch/module/module.c b/kernel/patch/module/module.c index e852cd5..75821ef 100644 --- a/kernel/patch/module/module.c +++ b/kernel/patch/module/module.c @@ -562,15 +562,15 @@ long load_module(const void *data, int len, const char *args, const char *event, flush_icache_all(); - /* Mark this KPM as loading (for crash detection) */ - kpm_safety_mark_loading(info->info.name); - - /* init is mandatory; an exit callback is optional. Never jump to NULL. */ + /* Validate before recording an in-progress load transaction. */ if (!mod->init || !*mod->init) { rc = -ENOEXEC; logkfe("KPM has no callable init entry point\n"); goto free; } + + /* Mark this KPM as loading (for crash detection) */ + kpm_safety_mark_loading(info->info.name); rc = (*mod->init)(mod->args, event, reserved); if (!rc) { From 1f6f565f2f877b0619b80ea871ca7cb80ee029e2 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:06:51 +0800 Subject: [PATCH 12/35] docs(runtime): record status-only native port and truthful capability reporting --- docs/PATCHNEST_DUAL_BACKEND.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/PATCHNEST_DUAL_BACKEND.md b/docs/PATCHNEST_DUAL_BACKEND.md index 1bce289..6607856 100644 --- a/docs/PATCHNEST_DUAL_BACKEND.md +++ b/docs/PATCHNEST_DUAL_BACKEND.md @@ -92,7 +92,7 @@ PatchNest WebUI / CLI <- 非常薄的控制面,无需常驻 ## 5. 当前分支完成范围 -- 已建立双后端的 **ABI v1 数据结构/能力标识**,不等于已经挂接到内核控制通道。 +- 已建立双后端的 **ABI v1 数据结构/能力标识**;新增 `ports/linux-native/` 的 Kconfig/Kbuild 开发入口与 `/dev/patchnest` **只读状态握手**。该入口固定报告 `NOT_READY`、`capabilities=0`,不代表已支持 KPM 加载,也尚未接入目标内核编译。 - 已修正现有 KPM loader 的几类独立风险:部分 ELF 边界、init/exit 空回调、模块枚举缓冲区、未知模块查询的 RCU 锁泄漏。 - 后续必须补齐:KPM 高并发引用/卸载协议,native port,两个实际 adapter,用户态 API 调用、Kbuild/AK3 构建和真机验证。 - **禁止发布可刷入稳定包或自动合并 `main`,直到这些条件完成。** From 8f9b04340ec1b4716c97b91ce35fe1b8cd04489e Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:36:41 +0800 Subject: [PATCH 13/35] fix(kpm): serialize publication and reject unsafe concurrent unloads --- kernel/patch/module/module.c | 88 ++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 20 deletions(-) diff --git a/kernel/patch/module/module.c b/kernel/patch/module/module.c index 75821ef..4e7f381 100644 --- a/kernel/patch/module/module.c +++ b/kernel/patch/module/module.c @@ -22,6 +22,7 @@ #include #include #include +/* The KPM list uses a short spinlock, not RCU callback lifetime. */ #include "module.h" #include "relo.h" @@ -512,6 +513,35 @@ static int elf_header_check(struct load_info *info) struct module modules = { 0 }; static spinlock_t module_lock; +/* + * KPM lifetime rules: module_lock protects list membership, callback_users + * and module_load_inflight. Module callbacks MUST run with the lock released. + * An unload during an active callback returns -EBUSY (never frees live code). + * This avoids blocking or sleeping in an RCU read-side critical section. + */ +#define PATCHNEST_MAX_KPM_MODULES 64 +static unsigned int module_count; +static bool module_load_inflight; + +static struct module *module_acquire(const char *name) +{ + struct module *mod; + + if (!name) return NULL; + spin_lock(&module_lock); + mod = find_module(name); /* Must be called with module_lock held. */ + if (mod) mod->callback_users++; + spin_unlock(&module_lock); + return mod; +} + +static void module_release(struct module *mod) +{ + spin_lock(&module_lock); + if (mod->callback_users) mod->callback_users--; + spin_unlock(&module_lock); +} + long load_module(const void *data, int len, const char *args, const char *event, void *__user reserved) { struct load_info load_info = { .len = len, .hdr = data }; @@ -534,14 +564,25 @@ long load_module(const void *data, int len, const char *args, const char *event, goto out; } - if (find_module(info->info.name)) { - logkfd("%s exist\n", info->info.name); + /* Reserve the single loader slot. Never hold a spinlock across init. */ + spin_lock(&module_lock); + if (module_load_inflight) { + rc = -EBUSY; + } else if (find_module(info->info.name)) { rc = -EEXIST; - goto out; + } else if (module_count >= PATCHNEST_MAX_KPM_MODULES) { + rc = -ENOSPC; + } else { + module_load_inflight = true; } + spin_unlock(&module_lock); + if (rc) goto out; struct module *mod = (struct module *)vmalloc(sizeof(struct module)); - if (!mod) return -ENOMEM; + if (!mod) { + rc = -ENOMEM; + goto clear_loader; + } memset(mod, 0, sizeof(struct module)); if (args) { @@ -575,8 +616,11 @@ long load_module(const void *data, int len, const char *args, const char *event, if (!rc) { logkfi("[%s] succeed with [%s] \n", mod->info.name, args); + spin_lock(&module_lock); list_add_tail(&mod->list, &modules.list); - goto out; + module_count++; + spin_unlock(&module_lock); + goto clear_loader; } else { logkfi("[%s] failed with [%s] error: %d, try exit ...\n", mod->info.name, args, rc); if (mod->exit && *mod->exit) @@ -588,6 +632,10 @@ long load_module(const void *data, int len, const char *args, const char *event, kp_free_exec(mod->start); free1: kvfree(mod); +clear_loader: + spin_lock(&module_lock); + module_load_inflight = false; + spin_unlock(&module_lock); out: return rc; } @@ -595,32 +643,32 @@ long load_module(const void *data, int len, const char *args, const char *event, // todo: lock long unload_module(const char *name, void *__user reserved) { - if (!name) return -EINVAL; - logkfe("name: %s\n", name); - - rcu_read_lock(); + struct module *mod; long rc = 0; - struct module *mod = find_module(name); + if (!name) return -EINVAL; + spin_lock(&module_lock); + mod = find_module(name); if (!mod) { rc = -ENOENT; - goto out; + } else if (mod->callback_users) { + /* Do not free executable memory while control/event is running. */ + rc = -EBUSY; + } else { + list_del(&mod->list); + module_count--; } - list_del(&mod->list); - if (mod->exit && *mod->exit) { + spin_unlock(&module_lock); + if (rc) return rc; + + /* Detached: nobody can obtain new callback references. */ + if (mod->exit && *mod->exit) rc = (*mod->exit)(reserved); - } if (mod->args) kvfree(mod->args); if (mod->ctl_args) kvfree(mod->ctl_args); - kp_free_exec(mod->start); kvfree(mod); - - logkfi("name: %s, rc: %d\n", name, rc); - -out: - rcu_read_unlock(); return rc; } From 7825d5de7fd489c74fe907c088ce8aeb3e152200 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:37:08 +0800 Subject: [PATCH 14/35] fix(kpm): record in-flight module callbacks for unload guard --- kernel/patch/include/module.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kernel/patch/include/module.h b/kernel/patch/include/module.h index 7b7dadd..fd4e783 100644 --- a/kernel/patch/include/module.h +++ b/kernel/patch/include/module.h @@ -50,6 +50,12 @@ struct module void *start; + /* + * Protected by module_lock in patch/module/module.c. Unload refuses + * with -EBUSY until all in-flight event/control callbacks finish. + * This field is NOT an atomic/free-standing reference count. + */ + unsigned int callback_users; struct list_head list; }; From 50f3d75ed4cbd16bf5ff6b4bece161c7517dfece Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:37:39 +0800 Subject: [PATCH 15/35] fix(kpm): pin control/event callbacks and protect module list with spinlock --- kernel/patch/module/module.c | 184 ++++++++++++++++------------------- 1 file changed, 85 insertions(+), 99 deletions(-) diff --git a/kernel/patch/module/module.c b/kernel/patch/module/module.c index 4e7f381..e3e87c8 100644 --- a/kernel/patch/module/module.c +++ b/kernel/patch/module/module.c @@ -712,70 +712,59 @@ long load_module_path(const char *path, const char *args, void *__user reserved) return rc; } -long module_control0(const char *name, const char *ctl_args, char *__user out_msg, int outlen) +/* + * A callback gets a pin under module_lock; it may sleep once the lock + * is dropped. Dynamic unload fails with -EBUSY rather than freeing code + * underneath an in-flight callback. ctl0 arguments are call-scoped. + */ +long module_control0(const char *name, const char *ctl_args, + char *__user out_msg, int outlen) { - if (!name || !ctl_args) return -EINVAL; - int args_len = strlen(ctl_args); - if (args_len <= 0) return -EINVAL; - - logkfi("name %s, args: %s\n", name, ctl_args); + struct module *mod; + char *local_args; + long rc; + size_t args_len; - long rc = 0; - rcu_read_lock(); + if (!name || !ctl_args || outlen < 0) return -EINVAL; + args_len = strlen(ctl_args); + if (!args_len || args_len > 4096) return -E2BIG; - struct module *mod = find_module(name); - if (!mod) { - rc = -ENOENT; - goto out; - } + mod = module_acquire(name); + if (!mod) return -ENOENT; if (!mod->ctl0 || !*mod->ctl0) { - logkfe("no ctl0\n"); rc = -ENOSYS; goto out; } - if (mod->ctl_args) kvfree(mod->ctl_args); - - mod->ctl_args = vmalloc(args_len + 1); - if (!mod->ctl_args) { + local_args = vmalloc(args_len + 1); + if (!local_args) { rc = -ENOMEM; goto out; } + memcpy(local_args, ctl_args, args_len + 1); + rc = (*mod->ctl0)(local_args, out_msg, outlen); + kvfree(local_args); - strcpy(mod->ctl_args, ctl_args); - - rc = (*mod->ctl0)(mod->ctl_args, out_msg, outlen); - - logkfi("name: %s, rc: %d\n", name, rc); out: - rcu_read_unlock(); + module_release(mod); return rc; } long module_control1(const char *name, void *a1, void *a2, void *a3) { - logkfi("name %s, a1: %llx, a2: %llx, a3: %llx\n", name, a1, a2, a3); - long rc = 0; - rcu_read_lock(); - - struct module *mod = find_module(name); - if (!mod) { - rc = -ENOENT; - goto out; - } + struct module *mod; + long rc; + if (!name) return -EINVAL; + mod = module_acquire(name); + if (!mod) return -ENOENT; if (!mod->ctl1 || !*mod->ctl1) { - logkfe("no ctl1\n"); rc = -ENOSYS; - goto out; + } else { + rc = (*mod->ctl1)(a1, a2, a3); } - - rc = (*mod->ctl1)(a1, a2, a3); - - logkfi("name: %s, rc: %d\n", name, rc); -out: - rcu_read_unlock(); + module_release(mod); return rc; } @@ -793,33 +782,26 @@ struct module *find_module(const char *name) int get_module_nums() { - rcu_read_lock(); - - struct module *pos; - int n = 0; - list_for_each_entry(pos, &modules.list, list) - { - n++; - } - rcu_read_unlock(); - - logkfd("%d\n", n); + int n; + spin_lock(&module_lock); + n = (int)module_count; + spin_unlock(&module_lock); return n; } int list_modules(char *out_names, int size) { - if (!out_names || size <= 0) return -EINVAL; - out_names[0] = '\0'; - rcu_read_lock(); - struct module *pos; int off = 0; - list_for_each_entry(pos, &modules.list, list) - { + + if (!out_names || size <= 0) return -EINVAL; + out_names[0] = '\0'; + spin_lock(&module_lock); + list_for_each_entry(pos, &modules.list, list) { int remaining = size - off; + int n; if (remaining <= 1) break; - int n = snprintf(out_names + off, remaining, "%s\n", pos->info.name); + n = snprintf(out_names + off, remaining, "%s\n", pos->info.name); if (n < 0) { off = n; break; @@ -831,41 +813,33 @@ int list_modules(char *out_names, int size) off += n; } if (off > 0 && out_names[off - 1] == '\n') out_names[off - 1] = '\0'; - - rcu_read_unlock(); + spin_unlock(&module_lock); return off; } int get_module_info(const char *name, char *out_info, int size) { - if (!name || !out_info || size <= 0) return -EINVAL; - rcu_read_lock(); + struct module *mod; + int sz; - struct module *mod = find_module(name); + if (!name || !out_info || size <= 0) return -EINVAL; + spin_lock(&module_lock); + mod = find_module(name); if (!mod) { - rcu_read_unlock(); + spin_unlock(&module_lock); return -ENOENT; } - - int sz = snprintf(out_info, size, - "name=%s\n" - "version=%s\n" - "license=%s\n" - "author=%s\n" - "description=%s\n" - "args=%s\n", - mod->info.name ?: "unknown", - mod->info.version ?: "0.0.0", - mod->info.license ?: "unknown", - mod->info.author ?: "unknown", - mod->info.description ?: "", - mod->args ?: ""); - + sz = snprintf(out_info, size, + "name=%s\nversion=%s\nlicense=%s\nauthor=%s\ndescription=%s\nargs=%s\n", + mod->info.name ?: "unknown", + mod->info.version ?: "0.0.0", + mod->info.license ?: "unknown", + mod->info.author ?: "unknown", + mod->info.description ?: "", + mod->args ?: ""); if (sz >= size) sz = size - 1; if (sz > 0 && out_info[sz - 1] == '\n') out_info[sz - 1] = '\0'; - logkfd("%s", out_info); - - rcu_read_unlock(); + spin_unlock(&module_lock); return sz; } @@ -892,33 +866,45 @@ void module_init() proc_hide_init(); } -int module_dispatch_event(enum kpm_event event, const char *source_name, const char *args) +int module_dispatch_event(enum kpm_event event, const char *source_name, + const char *args) { - if (event <= KPM_EVENT_NONE || event >= KPM_EVENT_MAX) return -EINVAL; - + struct module *snapshot[PATCHNEST_MAX_KPM_MODULES]; struct kpm_event_data data = { .event = event, .source_name = source_name, .args = args, .reserved = NULL, }; - - int count = 0; - rcu_read_lock(); struct module *pos; + unsigned int size = 0; + int dispatched = 0; + + if (event <= KPM_EVENT_NONE || event >= KPM_EVENT_MAX) + return -EINVAL; + + /* + * Take a bounded snapshot under module_lock. Pin each callback before + * releasing the lock; unload sees -EBUSY until all pins are dropped. + * Callbacks may sleep; never run them under RCU or a spinlock. + */ + spin_lock(&module_lock); list_for_each_entry(pos, &modules.list, list) { + if (size >= PATCHNEST_MAX_KPM_MODULES) break; + pos->callback_users++; + snapshot[size++] = pos; + } + spin_unlock(&module_lock); + + for (unsigned int i = 0; i < size; i++) { + pos = snapshot[i]; if (pos->event && *pos->event) { long rc = (*pos->event)(&data); - logkfi("event %d -> module %s: rc=%ld\n", event, pos->info.name, rc); - count++; - } - /* Backward compat: also call init with event string for old modules */ - if (pos->init && *pos->init && event == KPM_EVENT_POST_FS_DATA) { - /* Old modules already got event via init(args, "load-file") */ + logkfi("event %d -> module %s: rc=%ld\n", + event, pos->info.name, rc); + dispatched++; } + module_release(pos); } - rcu_read_unlock(); - - logkfi("dispatched event %d to %d modules\n", event, count); - return count; + return dispatched; } From f131d01f3c625ecd645bb58c86927caf2b99d5d5 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:40:04 +0800 Subject: [PATCH 16/35] feat(kpm): add reusable lock-protected callback lifetime primitive --- kernel/patch/include/kpm_lifetime.h | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 kernel/patch/include/kpm_lifetime.h diff --git a/kernel/patch/include/kpm_lifetime.h b/kernel/patch/include/kpm_lifetime.h new file mode 100644 index 0000000..f6d6361 --- /dev/null +++ b/kernel/patch/include/kpm_lifetime.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#ifndef PATCHNEST_KPM_LIFETIME_H +#define PATCHNEST_KPM_LIFETIME_H + +/* + * The caller MUST hold the registry lock for every operation below. + * Callbacks themselves execute without the registry lock. + * + * The unload operation is nonblocking: it returns -EBUSY if the module + * has active callback holders. No callback is allowed to execute after the + * module is detached from the registry. + */ +struct pn_kpm_lifetime { + unsigned int callback_users; +}; + +static inline int pn_kpm_lifetime_get(struct pn_kpm_lifetime *state) +{ + if (!state || state->callback_users == ~0u) + return 0; + ++state->callback_users; + return 1; +} + +static inline int pn_kpm_lifetime_put(struct pn_kpm_lifetime *state) +{ + if (!state || !state->callback_users) + return 0; + --state->callback_users; + return 1; +} + +static inline int pn_kpm_lifetime_may_unload(const struct pn_kpm_lifetime *state) +{ + return state && state->callback_users == 0; +} + +#endif /* PATCHNEST_KPM_LIFETIME_H */ From 512613b0b2161c0d704dd23e404b52b5accf2133 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:40:10 +0800 Subject: [PATCH 17/35] fix(kpm): validate ELF before parsing, bound metadata and close file on errors --- kernel/patch/module/module.c | 125 ++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 59 deletions(-) diff --git a/kernel/patch/module/module.c b/kernel/patch/module/module.c index e3e87c8..08e555d 100644 --- a/kernel/patch/module/module.c +++ b/kernel/patch/module/module.c @@ -48,20 +48,7 @@ static inline bool strstarts(const char *str, const char *prefix) return strncmp(str, prefix, strlen(prefix)) == 0; } -static char *next_string(char *string, unsigned long *secsize) -{ - while (string[0]) { - string++; - if ((*secsize)-- <= 1) return 0; - } - while (!string[0]) { - string++; - if ((*secsize)-- <= 1) return 0; - } - return string; -} - -/* Update size with this section: return offset. */ +/* ELF section layout has already been checked by elf_header_check(). */ static long get_offset(struct module *mod, unsigned int *size, Elf_Shdr *sechdr, unsigned int section) { long ret = ALIGN(*size, sechdr->sh_addralign ?: 1); @@ -69,26 +56,33 @@ static long get_offset(struct module *mod, unsigned int *size, Elf_Shdr *sechdr, return ret; } -static char *get_next_modinfo(const struct load_info *info, const char *tag, char *prev) -{ - char *p; - unsigned int taglen = strlen(tag); - Elf_Shdr *infosec = &info->sechdrs[info->index.info]; - unsigned long size = infosec->sh_size; - char *modinfo = (char *)info->hdr + infosec->sh_offset; - if (prev) { - size -= prev - modinfo; - modinfo = next_string(prev, &size); - } - for (p = modinfo; p; p = next_string(p, &size)) { - if (strncmp(p, tag, taglen) == 0 && p[taglen] == '=') return p + taglen + 1; - } - return 0; -} - +/* Never use strlen() on an untrusted ELF .modinfo/.kpm.info record. */ static char *get_modinfo(const struct load_info *info, const char *tag) { - return get_next_modinfo(info, tag, 0); + const Elf_Shdr *sec; + const char *cursor, *end; + size_t left, tag_len; + + if (!info || !tag || !info->index.info || + info->index.info >= info->hdr->e_shnum) + return NULL; + sec = &info->sechdrs[info->index.info]; + cursor = (const char *)info->hdr + sec->sh_offset; + left = sec->sh_size; + tag_len = strlen(tag); + + while (left) { + size_t item_len; + end = memchr(cursor, '\0', left); + if (!end) return NULL; /* Unterminated metadata is malformed. */ + item_len = (size_t)(end - cursor); + if (item_len > tag_len && cursor[tag_len] == '=' && + !memcmp(cursor, tag, tag_len)) + return (char *)cursor + tag_len + 1; + cursor = end + 1; + left -= item_len + 1; + } + return NULL; } static int find_sec(const struct load_info *info, const char *name) @@ -114,7 +108,7 @@ static unsigned long get_sh_size(struct load_info *info, const char *secname) int idx = find_sec(info, secname); if (!idx) return 0; Elf_Shdr *infosec = &info->sechdrs[idx]; - return infosec->sh_entsize; + return infosec->sh_size; } static void layout_sections(struct module *mod, struct load_info *info) @@ -520,6 +514,8 @@ static spinlock_t module_lock; * This avoids blocking or sleeping in an RCU read-side critical section. */ #define PATCHNEST_MAX_KPM_MODULES 64 +#define PATCHNEST_KPM_NAME_MAX 64 +#define PATCHNEST_KPM_FILE_MAX (16 * 1024 * 1024) static unsigned int module_count; static bool module_load_inflight; @@ -549,7 +545,17 @@ long load_module(const void *data, int len, const char *args, const char *event, long rc = 0; if ((rc = elf_header_check(info))) goto out; + if ((rc = kpm_safety_validate(data, len))) goto out; if ((rc = setup_load_info(info))) goto out; + if (!info->info.name || + strnlen(info->info.name, PATCHNEST_KPM_NAME_MAX) >= PATCHNEST_KPM_NAME_MAX) { + rc = -EINVAL; + goto out; + } + if (args && strnlen(args, 4097) > 4096) { + rc = -E2BIG; + goto out; + } /* Safety: check blacklist */ if (kpm_safety_check_blacklist(info->info.name)) { @@ -558,12 +564,6 @@ long load_module(const void *data, int len, const char *args, const char *event, goto out; } - /* Safety: validate ELF structure */ - if ((rc = kpm_safety_validate(data, len))) { - logkfe("kpm_safety: validation failed for %s\n", info->info.name); - goto out; - } - /* Reserve the single loader slot. Never hold a spinlock across init. */ spin_lock(&module_lock); if (module_load_inflight) { @@ -675,40 +675,47 @@ long unload_module(const char *name, void *__user reserved) long load_module_path(const char *path, const char *args, void *__user reserved) { long rc = 0; - logkfd("%s\n", path); - if (!path) return -EINVAL; + struct file *filp; + void *data; + loff_t len; + loff_t pos = 0; - struct file *filp = filp_open(path, O_RDONLY, 0); + if (!path) return -EINVAL; + logkfd("%s\n", path); + filp = filp_open(path, O_RDONLY, 0); if (unlikely(!filp || IS_ERR(filp))) { logkfe("open module: %s error\n", path); rc = PTR_ERR(filp); goto out; } - loff_t len = vfs_llseek(filp, 0, SEEK_END); - logkfd("module size: %llx\n", len); - vfs_llseek(filp, 0, SEEK_SET); + len = vfs_llseek(filp, 0, SEEK_END); + if (len < (loff_t)sizeof(Elf_Ehdr) || len > PATCHNEST_KPM_FILE_MAX) { + rc = len < 0 ? (long)len : -EFBIG; + goto close_file; + } + if (vfs_llseek(filp, 0, SEEK_SET) < 0) { + rc = -EIO; + goto close_file; + } - void *data = vmalloc(len); + data = vmalloc((size_t)len); if (!data) { rc = -ENOMEM; - goto out; + goto close_file; } - memset(data, 0, len); - loff_t pos = 0; - kernel_read(filp, data, len, &pos); - filp_close(filp, 0); - - if (pos != len) { - logkfe("read module: %s error\n", path); - rc = -EIO; - goto free; + rc = kernel_read(filp, data, (size_t)len, &pos); + if (rc != len) { + rc = rc < 0 ? rc : -EIO; + goto free_data; } - rc = load_module(data, len, args, "load-file", reserved); -free: + /* Parsed and copied before closing the file; no stale file pointer. */ + rc = load_module(data, (int)len, args, "load-file", reserved); +free_data: kvfree(data); -out: +close_file: + filp_close(filp, 0); return rc; } From c6b11f03191157ac0df8381115ca88a94fceeb92 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:40:11 +0800 Subject: [PATCH 18/35] refactor(kpm): store shared lifetime primitive per module --- kernel/patch/include/module.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kernel/patch/include/module.h b/kernel/patch/include/module.h index fd4e783..69c808f 100644 --- a/kernel/patch/include/module.h +++ b/kernel/patch/include/module.h @@ -8,6 +8,7 @@ #include #include +#include #include struct load_info @@ -55,7 +56,7 @@ struct module * with -EBUSY until all in-flight event/control callbacks finish. * This field is NOT an atomic/free-standing reference count. */ - unsigned int callback_users; + struct pn_kpm_lifetime lifetime; struct list_head list; }; From b1cca86b9e4284bb080a0d7cef3d1a250b23790c Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:40:21 +0800 Subject: [PATCH 19/35] refactor(kpm): use single verified reference-lifetime primitive --- kernel/patch/module/module.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/kernel/patch/module/module.c b/kernel/patch/module/module.c index 08e555d..8b982d3 100644 --- a/kernel/patch/module/module.c +++ b/kernel/patch/module/module.c @@ -526,7 +526,7 @@ static struct module *module_acquire(const char *name) if (!name) return NULL; spin_lock(&module_lock); mod = find_module(name); /* Must be called with module_lock held. */ - if (mod) mod->callback_users++; + if (mod && !pn_kpm_lifetime_get(&mod->lifetime)) mod = NULL; spin_unlock(&module_lock); return mod; } @@ -534,7 +534,7 @@ static struct module *module_acquire(const char *name) static void module_release(struct module *mod) { spin_lock(&module_lock); - if (mod->callback_users) mod->callback_users--; + if (!pn_kpm_lifetime_put(&mod->lifetime)) logkfe("KPM callback reference underflow\n"); spin_unlock(&module_lock); } @@ -651,7 +651,7 @@ long unload_module(const char *name, void *__user reserved) mod = find_module(name); if (!mod) { rc = -ENOENT; - } else if (mod->callback_users) { + } else if (!pn_kpm_lifetime_may_unload(&mod->lifetime)) { /* Do not free executable memory while control/event is running. */ rc = -EBUSY; } else { @@ -898,8 +898,8 @@ int module_dispatch_event(enum kpm_event event, const char *source_name, spin_lock(&module_lock); list_for_each_entry(pos, &modules.list, list) { if (size >= PATCHNEST_MAX_KPM_MODULES) break; - pos->callback_users++; - snapshot[size++] = pos; + if (pn_kpm_lifetime_get(&pos->lifetime)) + snapshot[size++] = pos; } spin_unlock(&module_lock); From 8f874614b050f6c94412d484e402976dbedd22cc Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:40:39 +0800 Subject: [PATCH 20/35] test(kpm): exercise real lifetime primitive with concurrent pinned callbacks --- tests/kpm_lifetime_concurrency.c | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/kpm_lifetime_concurrency.c diff --git a/tests/kpm_lifetime_concurrency.c b/tests/kpm_lifetime_concurrency.c new file mode 100644 index 0000000..e9f2c22 --- /dev/null +++ b/tests/kpm_lifetime_concurrency.c @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Host-side deterministic concurrent KPM lifetime test. + * + * Tests the SAME pn_kpm_lifetime primitive used by module.c, with a + * pthread mutex standing in for the kernel spinlock. It is not a kernel + * scheduler or real-device stress test. + */ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include + +#include "../kernel/patch/include/kpm_lifetime.h" + +enum { THREADS = 8 }; +static struct pn_kpm_lifetime lifetime = { 0 }; +static pthread_mutex_t registry_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_barrier_t inside_callback; +static pthread_barrier_t allow_return; + +static void *callback(void *arg) +{ + (void)arg; + pthread_mutex_lock(®istry_lock); + assert(pn_kpm_lifetime_get(&lifetime) == 1); + pthread_mutex_unlock(®istry_lock); + + /* Every callback must be active when the main thread tries unload. */ + pthread_barrier_wait(&inside_callback); + pthread_barrier_wait(&allow_return); + + pthread_mutex_lock(®istry_lock); + assert(pn_kpm_lifetime_put(&lifetime) == 1); + pthread_mutex_unlock(®istry_lock); + return NULL; +} + +int main(void) +{ + pthread_t workers[THREADS]; + assert(pthread_barrier_init(&inside_callback, NULL, THREADS + 1) == 0); + assert(pthread_barrier_init(&allow_return, NULL, THREADS + 1) == 0); + + assert(pn_kpm_lifetime_put(&lifetime) == 0); /* underflow refused */ + + for (unsigned int i = 0; i < THREADS; ++i) + assert(pthread_create(&workers[i], NULL, callback, NULL) == 0); + + pthread_barrier_wait(&inside_callback); + pthread_mutex_lock(®istry_lock); + assert(lifetime.callback_users == THREADS); + assert(pn_kpm_lifetime_may_unload(&lifetime) == 0); + pthread_mutex_unlock(®istry_lock); + + pthread_barrier_wait(&allow_return); + for (unsigned int i = 0; i < THREADS; ++i) + assert(pthread_join(workers[i], NULL) == 0); + + pthread_mutex_lock(®istry_lock); + assert(lifetime.callback_users == 0); + assert(pn_kpm_lifetime_may_unload(&lifetime) == 1); + pthread_mutex_unlock(®istry_lock); + + lifetime.callback_users = UINT_MAX; + assert(pn_kpm_lifetime_get(&lifetime) == 0); /* overflow refused */ + lifetime.callback_users = 0; + + pthread_barrier_destroy(&inside_callback); + pthread_barrier_destroy(&allow_return); + puts("PatchNest KPM lifetime concurrency: PASS"); + return 0; +} From 0ddf90d7355bf96cb0dedfbdf930ab5b2bed8d9b Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:40:51 +0800 Subject: [PATCH 21/35] ci(kpm): run GCC/Clang pthread concurrency test on actual lifetime primitive --- .github/workflows/patchnest-abi.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/patchnest-abi.yml b/.github/workflows/patchnest-abi.yml index cf2a48b..821735c 100644 --- a/.github/workflows/patchnest-abi.yml +++ b/.github/workflows/patchnest-abi.yml @@ -8,6 +8,10 @@ on: paths: - 'include/patchnest/**' - 'tests/patchnest_abi_smoke.c' + - 'tests/kpm_lifetime_concurrency.c' + - 'kernel/patch/include/kpm_lifetime.h' + - 'kernel/patch/include/module.h' + - 'kernel/patch/module/module.c' - '.github/workflows/patchnest-abi.yml' workflow_dispatch: @@ -28,3 +32,13 @@ jobs: tests/patchnest_abi_smoke.c -o /tmp/patchnest-abi-"$cc" /tmp/patchnest-abi-"$cc" done + - name: Exercise shared KPM lifetime primitive with concurrent callbacks + shell: bash + run: | + set -euo pipefail + for cc in gcc clang; do + "$cc" -std=c11 -O1 -g -pthread -Wall -Wextra -Werror \ + -fsanitize=undefined -fno-sanitize-recover=all \ + tests/kpm_lifetime_concurrency.c -o /tmp/patchnest-lifetime-"$cc" + /tmp/patchnest-lifetime-"$cc" + done From a7148896d0211d82d1924290d54f78ef2879604b Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:42:00 +0800 Subject: [PATCH 22/35] feat(runtime): add read-only native backend status probe CLI --- tools/patchnest_status.c | 63 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tools/patchnest_status.c diff --git a/tools/patchnest_status.c b/tools/patchnest_status.c new file mode 100644 index 0000000..2b984c9 --- /dev/null +++ b/tools/patchnest_status.c @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * PatchNest built-in backend status probe. Read-only by construction. + * This tool MUST NOT mistake successful root access for KPM support. + * + * For patched kernels, an eventual patched-backend adapter will provide + * the same ABI data; do not infer runtime capabilities from kpatch hello. + */ +#include +#include +#include +#include +#include +#include + +#include "../include/patchnest/runtime_abi.h" + +int main(int argc, char **argv) +{ + struct patchnest_status_v1 status = { + .struct_size = sizeof(struct patchnest_status_v1), + .abi_version = PATCHNEST_ABI_VERSION, + }; + int fd; + int rc; + + (void)argv; + if (argc != 1) { + fprintf(stderr, "usage: patchnest-status\n"); + return 2; + } + + fd = open("/dev/patchnest", O_RDONLY | O_CLOEXEC); + if (fd < 0) { + fprintf(stderr, "PatchNest native control device unavailable: %s\n", + strerror(errno)); + return errno == ENOENT ? 4 : 6; + } + + rc = ioctl(fd, PATCHNEST_IOCTL_STATUS, &status); + if (rc < 0) { + int saved_errno = errno; + fprintf(stderr, "PatchNest ABI negotiation failed: %s\n", + strerror(saved_errno)); + close(fd); + return saved_errno == ENOTTY || saved_errno == EPROTONOSUPPORT ? 4 : 6; + } + close(fd); + + if (status.struct_size != sizeof(status) || + (status.abi_version >> 16) != PATCHNEST_ABI_MAJOR || + (status.abi_version & 0xffffu) != PATCHNEST_ABI_MINOR || + status.backend != PATCHNEST_BACKEND_BUILTIN) { + fprintf(stderr, "PatchNest backend returned incompatible status\n"); + return 4; + } + + printf("abi=%u.%u\nbackend=native\ncapabilities=0x%x\nsafety=%u\n" + "loaded=%u\n", + status.abi_version >> 16, status.abi_version & 0xffffu, + status.capabilities, status.safety_state, status.loaded_kpm_count); + return 0; +} From fb55a64d1a00a5fff49d3b7e43ace0dd1b0cccf7 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:42:27 +0800 Subject: [PATCH 23/35] fix(runtime): use POSIX feature macros for portable status probe --- tools/patchnest_status.c | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/patchnest_status.c b/tools/patchnest_status.c index 2b984c9..5bf983b 100644 --- a/tools/patchnest_status.c +++ b/tools/patchnest_status.c @@ -6,6 +6,7 @@ * For patched kernels, an eventual patched-backend adapter will provide * the same ABI data; do not infer runtime capabilities from kpatch hello. */ +#define _POSIX_C_SOURCE 200809L #include #include #include From 7509cb2735c2d3c5603e02501e83867aa4e0a119 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:42:38 +0800 Subject: [PATCH 24/35] ci(runtime): build native status client and verify safe no-device behavior --- .github/workflows/patchnest-abi.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/patchnest-abi.yml b/.github/workflows/patchnest-abi.yml index 821735c..9cf782f 100644 --- a/.github/workflows/patchnest-abi.yml +++ b/.github/workflows/patchnest-abi.yml @@ -12,6 +12,7 @@ on: - 'kernel/patch/include/kpm_lifetime.h' - 'kernel/patch/include/module.h' - 'kernel/patch/module/module.c' + - 'tools/patchnest_status.c' - '.github/workflows/patchnest-abi.yml' workflow_dispatch: @@ -42,3 +43,15 @@ jobs: tests/kpm_lifetime_concurrency.c -o /tmp/patchnest-lifetime-"$cc" /tmp/patchnest-lifetime-"$cc" done + - name: Compile status-only native client and verify missing-device behavior + shell: bash + run: | + set -euo pipefail + for cc in gcc clang; do + "$cc" -std=c11 -O2 -Wall -Wextra -Werror \ + tools/patchnest_status.c -o /tmp/patchnest-status-"$cc" + rc=0 + /tmp/patchnest-status-"$cc" >/tmp/pn-output 2>/tmp/pn-error || rc=$? + test "$rc" -eq 4 + test ! -s /tmp/pn-output + done From f49f2774a25bd26ac255fa247703dcf24230641d Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:43:15 +0800 Subject: [PATCH 25/35] fix(kpm): restore error exit label after bounded file loader refactor --- kernel/patch/module/module.c | 1 + 1 file changed, 1 insertion(+) diff --git a/kernel/patch/module/module.c b/kernel/patch/module/module.c index 8b982d3..1e090d6 100644 --- a/kernel/patch/module/module.c +++ b/kernel/patch/module/module.c @@ -716,6 +716,7 @@ long load_module_path(const char *path, const char *args, void *__user reserved) kvfree(data); close_file: filp_close(filp, 0); +out: return rc; } From f15cf382d04ba4883245d6dbe0bef4fc04e26f66 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:47:49 +0800 Subject: [PATCH 26/35] fix(kpm): cap ELF memory layout and verify symbol and relocation bounds --- kernel/patch/module/module.c | 66 ++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/kernel/patch/module/module.c b/kernel/patch/module/module.c index 1e090d6..fbcfb40 100644 --- a/kernel/patch/module/module.c +++ b/kernel/patch/module/module.c @@ -471,6 +471,13 @@ static int elf_header_check(struct load_info *info) hdr->e_shstrndx == SHN_UNDEF || hdr->e_shstrndx >= hdr->e_shnum) return -ENOEXEC; + /* + * Keep an upper bound on ELF metadata and all executable/data mappings. + * Even SHT_NOBITS consumes memory at relocation time. + */ + if (len > (16u * 1024u * 1024u) || hdr->e_shnum > 1024) + return -EFBIG; + /* Check bounds before setup_load_info dereferences the section table. */ if (hdr->e_shoff > len || hdr->e_shnum > (len - hdr->e_shoff) / sizeof(Elf_Shdr)) @@ -483,8 +490,23 @@ static int elf_header_check(struct load_info *info) return -ENOEXEC; const char *shstrtab = (const char *)hdr + names->sh_offset; + unsigned long long mapped_budget = 0; for (unsigned int i = 0; i < hdr->e_shnum; ++i) { const Elf_Shdr *sec = §ions[i]; + + /* Alignment is always power of two, not user-controlled padding. */ + if (sec->sh_addralign && + ((sec->sh_addralign & (sec->sh_addralign - 1)) != 0 || + sec->sh_addralign > 65536)) + return -ENOEXEC; + if (sec->sh_flags & SHF_ALLOC) { + /* Includes NOBITS, which has no ELF file payload. */ + if (sec->sh_size > (32u * 1024u * 1024u)) + return -EFBIG; + mapped_budget += sec->sh_size + (sec->sh_addralign ?: 1); + if (mapped_budget > (64u * 1024u * 1024u)) + return -EFBIG; + } if (sec->sh_type != SHT_NOBITS && (sec->sh_offset > len || sec->sh_size > len - sec->sh_offset)) return -ENOEXEC; @@ -497,9 +519,49 @@ static int elf_header_check(struct load_info *info) sec->sh_entsize != sizeof(Elf_Sym) || (sec->sh_size % sizeof(Elf_Sym)) != 0)) return -ENOEXEC; - if ((sec->sh_type == SHT_REL || sec->sh_type == SHT_RELA) && - (sec->sh_info >= hdr->e_shnum || sec->sh_link >= hdr->e_shnum)) + if (sec->sh_type == SHT_SYMTAB) { + const Elf_Shdr *strings = §ions[sec->sh_link]; + const char *strtab = (const char *)hdr + strings->sh_offset; + const Elf_Sym *syms = (const void *)((const char *)hdr + sec->sh_offset); + unsigned long count = sec->sh_size / sizeof(*syms); + + if (!strings->sh_size) return -ENOEXEC; + for (unsigned long j = 0; j < count; j++) { + const Elf_Sym *sym = &syms[j]; + if (sym->st_name >= strings->sh_size || + !memchr(strtab + sym->st_name, '\0', + strings->sh_size - sym->st_name)) + return -ENOEXEC; + if (sym->st_shndx >= hdr->e_shnum && + sym->st_shndx != SHN_UNDEF && + sym->st_shndx != SHN_ABS && + sym->st_shndx != SHN_COMMON) + return -ENOEXEC; + } + } + /* AArch64 KPM uses RELA. REL is not implemented; fail closed. */ + if (sec->sh_type == SHT_REL) return -ENOEXEC; + if (sec->sh_type == SHT_RELA) { + if (sec->sh_info >= hdr->e_shnum || + sec->sh_link >= hdr->e_shnum || + sections[sec->sh_link].sh_type != SHT_SYMTAB || + sec->sh_entsize != sizeof(Elf64_Rela) || + (sec->sh_size % sizeof(Elf64_Rela)) != 0) + return -ENOEXEC; + const Elf_Shdr *target = §ions[sec->sh_info]; + const Elf_Shdr *symtab = §ions[sec->sh_link]; + const Elf64_Rela *relas = + (const void *)((const char *)hdr + sec->sh_offset); + unsigned long nrelas = sec->sh_size / sizeof(*relas); + unsigned long nsym = symtab->sh_size / sizeof(Elf_Sym); + for (unsigned long j = 0; j < nrelas; j++) { + if (ELF64_R_SYM(relas[j].r_info) >= nsym || + target->sh_size < 4 || + relas[j].r_offset > target->sh_size - 4) + return -ENOEXEC; + } + } } return 0; } From a41bae843ddb67d512a7805a9995218712be3d65 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:48:09 +0800 Subject: [PATCH 27/35] fix(kpm): validate linked string-table bounds before dereference --- kernel/patch/module/module.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kernel/patch/module/module.c b/kernel/patch/module/module.c index fbcfb40..6a261d1 100644 --- a/kernel/patch/module/module.c +++ b/kernel/patch/module/module.c @@ -521,6 +521,10 @@ static int elf_header_check(struct load_info *info) return -ENOEXEC; if (sec->sh_type == SHT_SYMTAB) { const Elf_Shdr *strings = §ions[sec->sh_link]; + /* The linked string table may appear later in the section list. */ + if (strings->sh_offset > len || + strings->sh_size > len - strings->sh_offset) + return -ENOEXEC; const char *strtab = (const char *)hdr + strings->sh_offset; const Elf_Sym *syms = (const void *)((const char *)hdr + sec->sh_offset); unsigned long count = sec->sh_size / sizeof(*syms); From 1a93baed8618f3b9dcb1cca8ed70cbb7c63dbbda Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:48:58 +0800 Subject: [PATCH 28/35] fix(kpm): enforce architecture relocation write width within target section --- kernel/patch/module/module.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/kernel/patch/module/module.c b/kernel/patch/module/module.c index 6a261d1..7a81a96 100644 --- a/kernel/patch/module/module.c +++ b/kernel/patch/module/module.c @@ -4,6 +4,7 @@ */ #include +#include #include #include #include @@ -560,9 +561,18 @@ static int elf_header_check(struct load_info *info) unsigned long nrelas = sec->sh_size / sizeof(*relas); unsigned long nsym = symtab->sh_size / sizeof(Elf_Sym); for (unsigned long j = 0; j < nrelas; j++) { + unsigned int type = ELF64_R_TYPE(relas[j].r_info); + unsigned int access_width = 4; /* AArch64 instructions */ + if (type == R_AARCH64_NONE) + access_width = 0; + else if (type == R_AARCH64_ABS64 || type == R_AARCH64_PREL64) + access_width = 8; + else if (type == R_AARCH64_ABS16 || type == R_AARCH64_PREL16) + access_width = 2; + if (ELF64_R_SYM(relas[j].r_info) >= nsym || - target->sh_size < 4 || - relas[j].r_offset > target->sh_size - 4) + target->sh_size < access_width || + relas[j].r_offset > target->sh_size - access_width) return -ENOEXEC; } } From e1dcefd9d0d342d3715289eb0f820a7143959b5a Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 03:49:53 +0800 Subject: [PATCH 29/35] docs(runtime): record protected callbacks, ELF bounds and remaining hook lifetime risk --- docs/PATCHNEST_DUAL_BACKEND.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/PATCHNEST_DUAL_BACKEND.md b/docs/PATCHNEST_DUAL_BACKEND.md index 6607856..dc800ec 100644 --- a/docs/PATCHNEST_DUAL_BACKEND.md +++ b/docs/PATCHNEST_DUAL_BACKEND.md @@ -93,8 +93,11 @@ PatchNest WebUI / CLI <- 非常薄的控制面,无需常驻 ## 5. 当前分支完成范围 - 已建立双后端的 **ABI v1 数据结构/能力标识**;新增 `ports/linux-native/` 的 Kconfig/Kbuild 开发入口与 `/dev/patchnest` **只读状态握手**。该入口固定报告 `NOT_READY`、`capabilities=0`,不代表已支持 KPM 加载,也尚未接入目标内核编译。 -- 已修正现有 KPM loader 的几类独立风险:部分 ELF 边界、init/exit 空回调、模块枚举缓冲区、未知模块查询的 RCU 锁泄漏。 -- 后续必须补齐:KPM 高并发引用/卸载协议,native port,两个实际 adapter,用户态 API 调用、Kbuild/AK3 构建和真机验证。 +- 已实现 KPM 回调引用保护的第一阶段:列表自旋锁、单实例加载预留、控制回调独立参数、最多 64 个模块的事件快照,以及忙时 `-EBUSY` 卸载保护。共用引用计数基础逻辑使用 GCC/Clang + pthread 进行并发测试。 +- 已扩展 ELF 防御:大小、总内存映射预算、节对齐、符号表字符串终止、RELA 目标及索引/写入宽度边界。尚需变异样本 fuzz 与完整重定位语义审计。 +- 已添加 `tools/patchnest_status.c`(native `ioctl` 只读状态客户端),管理端 `Zhanfg/PatchNest` 的 Draft PR #12 提供只读运行时探测和展示。 +- **重要限制**:现有 callback pin 只覆盖 `control0/control1/event`,未覆盖 KPM 注册的所有外部 inline hook、异步工作任务与其他任意函数入口。因此动态卸载的代码存活期仍不能宣称彻底安全,Issue #10 仍是 P0 阻塞项。 +- 后续必须补齐:KPM hook/工作任务全链路注销与同步、Native KPM loader、Patched adapter、统一认证 API、真正的 Kbuild/AK3 编译和真机验证。 - **禁止发布可刷入稳定包或自动合并 `main`,直到这些条件完成。** ## 6. 版本与仓库治理 From cbaf84c29c7f17d21527eba36f8544938bb0f796 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 21:08:02 +0800 Subject: [PATCH 30/35] feat(native): add idempotent opt-in Linux Kbuild integration script --- ports/linux-native/integrate.sh | 76 +++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 ports/linux-native/integrate.sh diff --git a/ports/linux-native/integrate.sh b/ports/linux-native/integrate.sh new file mode 100644 index 0000000..5048d5d --- /dev/null +++ b/ports/linux-native/integrate.sh @@ -0,0 +1,76 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# Safely stage the PatchNest built-in status adapter into a kernel tree. +# No arguments. Set PATCHNEST_KERNEL_SOURCE and PATCHNEST_APPLY=1 to apply. +# This does not configure KPM or generate an AK3 archive. +set -eu +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +PROJECT_DIR=$(CDPATH= cd -- "$SELF_DIR/../.." && pwd) +KERNEL=${PATCHNEST_KERNEL_SOURCE:-} +if [ -z "$KERNEL" ]; then + if [ -t 0 ]; then + printf 'Kernel source directory: ' + IFS= read -r KERNEL + else + echo "ERROR: PATCHNEST_KERNEL_SOURCE is required in CI" >&2 + exit 2 + fi +fi + +[ -d "$KERNEL" ] || { echo "ERROR: no kernel source at $KERNEL" >&2; exit 2; } +KERNEL=$(CDPATH= cd -- "$KERNEL" && pwd) +case "$KERNEL" in + /|"$PROJECT_DIR"|"$PROJECT_DIR"/*) + echo "ERROR: refuse to alter root or PatchNest source" >&2; exit 2 ;; +esac +for required in drivers/Makefile drivers/Kconfig include/linux; do + [ -e "$KERNEL/$required" ] || { + echo "ERROR: incomplete Linux kernel tree ($required missing)" >&2 + exit 2 + } +done +grep -qE '(^|[[:space:]])ARM64|^#' "$KERNEL/drivers/Kconfig" || true +if [ "${PATCHNEST_APPLY:-0}" != "1" ]; then + echo "DRY RUN: validated kernel tree $KERNEL" + echo "Would install drivers/patchnest + include/linux/patchnest_runtime_abi.h" + echo "Would update drivers/{Makefile,Kconfig} idempotently." + echo "Set PATCHNEST_APPLY=1 to explicitly apply." + exit 0 +fi + +# Never silently replace another implementation that shares these names. +dst="$KERNEL/drivers/patchnest" +mkdir -p "$dst" +if [ -f "$dst/patchnest_native.c" ] && + ! cmp -s "$SELF_DIR/patchnest_native.c" "$dst/patchnest_native.c"; then + # The installed file has an adjusted include, compare a normalized copy. + tmp=$(mktemp) || exit 1 + sed 's|#include "../../include/patchnest/runtime_abi.h"|#include |' \ + "$SELF_DIR/patchnest_native.c" > "$tmp" + if ! cmp -s "$tmp" "$dst/patchnest_native.c"; then + rm -f "$tmp" + echo "ERROR: unknown existing native adapter; review conflicts manually" >&2 + exit 1 + fi + rm -f "$tmp" +fi + +cp "$SELF_DIR/Kconfig" "$dst/Kconfig" +cp "$SELF_DIR/Makefile" "$dst/Makefile" +tmp=$(mktemp) || exit 1 +trap 'rm -f "$tmp"' EXIT HUP INT TERM +sed 's|#include "../../include/patchnest/runtime_abi.h"|#include |' \ + "$SELF_DIR/patchnest_native.c" > "$tmp" +cp "$tmp" "$dst/patchnest_native.c" +cp "$PROJECT_DIR/include/patchnest/runtime_abi.h" \ + "$KERNEL/include/linux/patchnest_runtime_abi.h" + +if ! grep -Fq 'source "drivers/patchnest/Kconfig"' "$KERNEL/drivers/Kconfig"; then + printf '\nsource "drivers/patchnest/Kconfig"\n' >> "$KERNEL/drivers/Kconfig" +fi +if ! grep -Fq 'obj-$(CONFIG_PATCHNEST_CORE) += patchnest/' "$KERNEL/drivers/Makefile"; then + printf '\nobj-$(CONFIG_PATCHNEST_CORE) += patchnest/\n' >> "$KERNEL/drivers/Makefile" +fi + +echo "Integrated status-only driver at: $KERNEL/drivers/patchnest" +echo "CONFIG_PATCHNEST_CORE is default n. This does NOT enable KPM loading." From 9e2c63a3c4bbddbae140f241a3514dba08f1018f Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 21:08:36 +0800 Subject: [PATCH 31/35] test(native): stage driver twice and reject conflicting existing kernel code --- tests/patchnest_native_integration.sh | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/patchnest_native_integration.sh diff --git a/tests/patchnest_native_integration.sh b/tests/patchnest_native_integration.sh new file mode 100644 index 0000000..9dc0344 --- /dev/null +++ b/tests/patchnest_native_integration.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# Host-only Kbuild staging regression. Does not compile a Linux kernel. +set -eu +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT HUP INT TERM +mkdir -p "$tmp/linux/drivers" "$tmp/linux/include/linux" +printf 'menu "Device Drivers"\nendmenu\n' > "$tmp/linux/drivers/Kconfig" +printf 'obj-y += base/\n' > "$tmp/linux/drivers/Makefile" +printf 'original\n' > "$tmp/linux/drivers/original.txt" +export PATCHNEST_KERNEL_SOURCE="$tmp/linux" +sh "$root/ports/linux-native/integrate.sh" > "$tmp/dryrun" +[ ! -e "$tmp/linux/drivers/patchnest" ] +PATCHNEST_APPLY=1 sh "$root/ports/linux-native/integrate.sh" +PATCHNEST_APPLY=1 sh "$root/ports/linux-native/integrate.sh" +[ "$(grep -Fc 'source "drivers/patchnest/Kconfig"' "$tmp/linux/drivers/Kconfig")" = 1 ] +[ "$(grep -Fc 'obj-$(CONFIG_PATCHNEST_CORE) += patchnest/' "$tmp/linux/drivers/Makefile")" = 1 ] +grep -Fq '#include ' "$tmp/linux/drivers/patchnest/patchnest_native.c" +cmp "$tmp/linux/drivers/original.txt" /dev/null >/dev/null && exit 1 || : +grep -Fq 'original' "$tmp/linux/drivers/original.txt" +# Conflicting preexisting driver must never be overwritten. +printf 'different implementation\n' > "$tmp/linux/drivers/patchnest/patchnest_native.c" +if PATCHNEST_APPLY=1 sh "$root/ports/linux-native/integrate.sh" >/dev/null 2>&1; then + echo "ERROR: conflicting implementation overwritten" >&2 + exit 1 +fi +echo "PASS: opt-in, idempotent integration and conflict rejection" From 3db3f5b68a377f6d5d9b22ce7b3f5a21c0e6c5ec Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 21:08:49 +0800 Subject: [PATCH 32/35] ci(native): verify idempotent kernel-tree installation and conflict guard --- .github/workflows/patchnest-abi.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/patchnest-abi.yml b/.github/workflows/patchnest-abi.yml index 9cf782f..d644947 100644 --- a/.github/workflows/patchnest-abi.yml +++ b/.github/workflows/patchnest-abi.yml @@ -13,6 +13,8 @@ on: - 'kernel/patch/include/module.h' - 'kernel/patch/module/module.c' - 'tools/patchnest_status.c' + - 'ports/linux-native/**' + - 'tests/patchnest_native_integration.sh' - '.github/workflows/patchnest-abi.yml' workflow_dispatch: @@ -43,6 +45,13 @@ jobs: tests/kpm_lifetime_concurrency.c -o /tmp/patchnest-lifetime-"$cc" /tmp/patchnest-lifetime-"$cc" done + - name: Verify non-destructive in-tree Kbuild integration + shell: bash + run: | + set -euo pipefail + dash -n ports/linux-native/integrate.sh + dash tests/patchnest_native_integration.sh + bash tests/patchnest_native_integration.sh - name: Compile status-only native client and verify missing-device behavior shell: bash run: | From f278faf14ff00201e5e7a66570aab81352cc8853 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 21:13:12 +0800 Subject: [PATCH 33/35] fix(native): reject ABI/Kbuild conflicts before modifying target kernel tree --- ports/linux-native/integrate.sh | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/ports/linux-native/integrate.sh b/ports/linux-native/integrate.sh index 5048d5d..ff1812c 100644 --- a/ports/linux-native/integrate.sh +++ b/ports/linux-native/integrate.sh @@ -38,9 +38,24 @@ if [ "${PATCHNEST_APPLY:-0}" != "1" ]; then exit 0 fi -# Never silently replace another implementation that shares these names. +# Preflight ALL destination files before creating anything. +# Reject symlinked adapter trees and conflicting prior implementations. dst="$KERNEL/drivers/patchnest" -mkdir -p "$dst" +abi_dst="$KERNEL/include/linux/patchnest_runtime_abi.h" +if [ -L "$dst" ] || [ -L "$abi_dst" ]; then + echo "ERROR: symlinked destination is not trusted" >&2 + exit 1 +fi +if [ -e "$abi_dst" ] && ! cmp -s "$PROJECT_DIR/include/patchnest/runtime_abi.h" "$abi_dst"; then + echo "ERROR: ABI header differs, refusing overwrite" >&2 + exit 1 +fi +for file in Kconfig Makefile; do + if [ -e "$dst/$file" ] && ! cmp -s "$SELF_DIR/$file" "$dst/$file"; then + echo "ERROR: existing $file differs, refusing overwrite" >&2 + exit 1 + fi +done if [ -f "$dst/patchnest_native.c" ] && ! cmp -s "$SELF_DIR/patchnest_native.c" "$dst/patchnest_native.c"; then # The installed file has an adjusted include, compare a normalized copy. @@ -55,6 +70,7 @@ if [ -f "$dst/patchnest_native.c" ] && rm -f "$tmp" fi +mkdir -p "$dst" cp "$SELF_DIR/Kconfig" "$dst/Kconfig" cp "$SELF_DIR/Makefile" "$dst/Makefile" tmp=$(mktemp) || exit 1 From 256349e7c3ec971520a90bf7d64cc6ced3e4d0b6 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 21:14:22 +0800 Subject: [PATCH 34/35] test(native): reject mismatched ABI headers before touching kernel tree --- tests/patchnest_native_integration.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/patchnest_native_integration.sh b/tests/patchnest_native_integration.sh index 9dc0344..29ad7e5 100644 --- a/tests/patchnest_native_integration.sh +++ b/tests/patchnest_native_integration.sh @@ -17,8 +17,15 @@ PATCHNEST_APPLY=1 sh "$root/ports/linux-native/integrate.sh" [ "$(grep -Fc 'source "drivers/patchnest/Kconfig"' "$tmp/linux/drivers/Kconfig")" = 1 ] [ "$(grep -Fc 'obj-$(CONFIG_PATCHNEST_CORE) += patchnest/' "$tmp/linux/drivers/Makefile")" = 1 ] grep -Fq '#include ' "$tmp/linux/drivers/patchnest/patchnest_native.c" -cmp "$tmp/linux/drivers/original.txt" /dev/null >/dev/null && exit 1 || : grep -Fq 'original' "$tmp/linux/drivers/original.txt" +# Conflicting ABI must be rejected without damaging the kernel tree. +printf 'unknown ABI\n' > "$tmp/linux/include/linux/patchnest_runtime_abi.h" +if PATCHNEST_APPLY=1 sh "$root/ports/linux-native/integrate.sh" >/dev/null 2>&1; then + echo "ERROR: conflicting ABI header overwritten" >&2 + exit 1 +fi +grep -Fq 'unknown ABI' "$tmp/linux/include/linux/patchnest_runtime_abi.h" +cp "$root/include/patchnest/runtime_abi.h" "$tmp/linux/include/linux/patchnest_runtime_abi.h" # Conflicting preexisting driver must never be overwritten. printf 'different implementation\n' > "$tmp/linux/drivers/patchnest/patchnest_native.c" if PATCHNEST_APPLY=1 sh "$root/ports/linux-native/integrate.sh" >/dev/null 2>&1; then From 71885de45e48adc4264ae94f7ed2d74cadce9ae7 Mon Sep 17 00:00:00 2001 From: Axymorrsen Date: Wed, 14 Oct 2026 21:15:13 +0800 Subject: [PATCH 35/35] docs(native): document fail-closed idempotent Kbuild staging and acceptance limits --- docs/PATCHNEST_DUAL_BACKEND.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/PATCHNEST_DUAL_BACKEND.md b/docs/PATCHNEST_DUAL_BACKEND.md index dc800ec..b6c6826 100644 --- a/docs/PATCHNEST_DUAL_BACKEND.md +++ b/docs/PATCHNEST_DUAL_BACKEND.md @@ -100,6 +100,14 @@ PatchNest WebUI / CLI <- 非常薄的控制面,无需常驻 - 后续必须补齐:KPM hook/工作任务全链路注销与同步、Native KPM loader、Patched adapter、统一认证 API、真正的 Kbuild/AK3 编译和真机验证。 - **禁止发布可刷入稳定包或自动合并 `main`,直到这些条件完成。** +### A 版 Linux 内核源码接入(本轮新增) + +`ports/linux-native/integrate.sh` 可以将 status-only native adapter 安装到一个完整 Linux ARM64 内核源码树。脚本默认 **dry run**,且在任何复制发生前执行既有实现冲突检查;需显式设置 `PATCHNEST_KERNEL_SOURCE` 与 `PATCHNEST_APPLY=1` 才会修改目标源码。可重复运行,不会重复附加 Kconfig / Makefile 条目。脚本在交互终端也支持直接 `sh` 无参数执行。 + +集成后,`CONFIG_PATCHNEST_CORE` 默认为 `n`。可显式改为 `y` 做目标内核编译验证;当前代码仍只提供 `/dev/patchnest` 状态握手,**绝不应该宣称可加载或嵌入 KPM**。内核源码树的构建、OnePlus 6/13 对应 AK3 打包、实机稳定性尚未完成。 + +CI 的 `tests/patchnest_native_integration.sh` 覆盖 dry-run 无副作用、重复接入幂等性、原始文件保持不变以及 ABI 和 driver 冲突拒绝。其通过不等于真正 Kbuild/GKI 链接完成。 + ## 6. 版本与仓库治理 - Core 改动仅在本仓库 feature 分支;WebUI/CLI 改动将进入 `Zhanfg/PatchNest` 的对应 feature 分支。