From ffb06b1bbcdc02deb69ea351fa02cee2d2f759cb Mon Sep 17 00:00:00 2001 From: Mohammed Riad <1@mhmrdd.me> Date: Wed, 22 Jul 2026 23:01:23 +0100 Subject: [PATCH 1/6] perf(kstorage): hash groups by did for O(1) lookup get_kstorage walked a per-group linked list, so every su check on the hot path was O(N) in the number of allowed uids. Replace each group with a fixed bucket array hashed on did, making lookup, update and remove O(1) on average. Traverse with the _rcu iterators to match the _rcu mutators, fix on_each_kstorage_elem returning 0 through a shadowed rc, and free the new entry on the memdup_user error path. --- kernel/patch/common/kstorage.c | 76 ++++++++++++++++++++------------- kernel/patch/include/kstorage.h | 2 +- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/kernel/patch/common/kstorage.c b/kernel/patch/common/kstorage.c index 9b5ab2bd..41bfe643 100644 --- a/kernel/patch/common/kstorage.c +++ b/kernel/patch/common/kstorage.c @@ -18,13 +18,22 @@ #define KSTRORAGE_MAX_GROUP_NUM 4 +#define KSTORAGE_HASH_BITS 8 +#define KSTORAGE_NBUCKETS (1 << KSTORAGE_HASH_BITS) + // static atomic64_t used_max_group = ATOMIC_INIT(0); static int used_max_group = -1; -static struct list_head kstorage_groups[KSTRORAGE_MAX_GROUP_NUM]; +static struct hlist_head kstorage_groups[KSTRORAGE_MAX_GROUP_NUM][KSTORAGE_NBUCKETS]; static spinlock_t kstorage_glocks[KSTRORAGE_MAX_GROUP_NUM]; static int group_sizes[KSTRORAGE_MAX_GROUP_NUM] = { 0 }; static spinlock_t used_max_group_lock; +static inline struct hlist_head *kstorage_bucket(int gid, long did) +{ + unsigned long h = (unsigned long)did * 0x9e3779b97f4a7c15UL; + return &kstorage_groups[gid][h >> (64 - KSTORAGE_HASH_BITS)]; +} + static void reclaim_callback(struct rcu_head *rcu) { struct kstorage *ks = container_of(rcu, struct kstorage, rcu); @@ -54,13 +63,13 @@ int write_kstorage(int gid, long did, void *data, int offset, int len, bool data int rc = -ENOENT; if (gid < 0 || gid >= KSTRORAGE_MAX_GROUP_NUM) return rc; - struct list_head *head = &kstorage_groups[gid]; + struct hlist_head *bucket = kstorage_bucket(gid, did); spinlock_t *lock = &kstorage_glocks[gid]; struct kstorage *pos = 0, *old = 0; rcu_read_lock(); - list_for_each_entry(pos, head, list) + hlist_for_each_entry_rcu(pos, bucket, hnode) { if (pos->did == did) { old = pos; @@ -68,7 +77,7 @@ int write_kstorage(int gid, long did, void *data, int offset, int len, bool data } } - struct kstorage *new = (struct kstorage *)vmalloc(sizeof(struct kstorage) + len); + struct kstorage *new = (struct kstorage *)vmalloc(sizeof(struct kstorage) + len); if (!new) { rcu_read_unlock(); return -ENOMEM; @@ -80,6 +89,7 @@ int write_kstorage(int gid, long did, void *data, int offset, int len, bool data void *drc = memdup_user(data + offset, len); if (IS_ERR(drc)) { rcu_read_unlock(); + kvfree(new); return PTR_ERR(drc); } memcpy(new->data, drc, len); @@ -91,9 +101,9 @@ int write_kstorage(int gid, long did, void *data, int offset, int len, bool data spin_lock(lock); if (old) { // update - list_replace_rcu(&old->list, &new->list); + hlist_replace_rcu(&old->hnode, &new->hnode); } else { // add new one - list_add_rcu(&new->list, head); + hlist_add_head_rcu(&new->hnode, bucket); group_sizes[gid]++; } spin_unlock(lock); @@ -117,10 +127,10 @@ const struct kstorage *get_kstorage(int gid, long did) { if (gid < 0 || gid >= KSTRORAGE_MAX_GROUP_NUM) return ERR_PTR(-ENOENT); - struct list_head *head = &kstorage_groups[gid]; + struct hlist_head *bucket = kstorage_bucket(gid, did); struct kstorage *pos = 0; - list_for_each_entry(pos, head, list) + hlist_for_each_entry_rcu(pos, bucket, hnode) { if (pos->did == did) { return pos; @@ -137,17 +147,19 @@ int on_each_kstorage_elem(int gid, on_kstorage_cb cb, void *udata) int rc = 0; - struct list_head *head = &kstorage_groups[gid]; struct kstorage *pos = 0; rcu_read_lock(); - list_for_each_entry(pos, head, list) - { - int rc = cb(pos, udata); - if (rc) break; + for (int b = 0; b < KSTORAGE_NBUCKETS; b++) { + hlist_for_each_entry_rcu(pos, &kstorage_groups[gid][b], hnode) + { + rc = cb(pos, udata); + if (rc) goto out; + } } +out: rcu_read_unlock(); return rc; @@ -189,27 +201,29 @@ int list_kstorage_ids(int gid, long *ids, int idslen, bool data_is_user) int cnt = 0; - struct list_head *head = &kstorage_groups[gid]; struct kstorage *pos = 0; rcu_read_lock(); - list_for_each_entry(pos, head, list) - { - if (cnt >= idslen) break; - - if (data_is_user) { - int cplen = compat_copy_to_user(ids + cnt, &pos->did, sizeof(pos->did)); - if (cplen <= 0) { - logkfe("compat_copy_to_user error: %d", cplen); - cnt = cplen; + for (int b = 0; b < KSTORAGE_NBUCKETS; b++) { + hlist_for_each_entry_rcu(pos, &kstorage_groups[gid][b], hnode) + { + if (cnt >= idslen) goto out; + + if (data_is_user) { + int cplen = compat_copy_to_user(ids + cnt, &pos->did, sizeof(pos->did)); + if (cplen <= 0) { + logkfe("compat_copy_to_user error: %d", cplen); + cnt = cplen; + } + } else { + memcpy(ids + cnt, &pos->did, sizeof(pos->did)); } - } else { - memcpy(ids + cnt, &pos->did, sizeof(pos->did)); + cnt++; } - cnt++; } +out: rcu_read_unlock(); return cnt; @@ -221,16 +235,16 @@ int remove_kstorage(int gid, long did) int rc = -ENOENT; if (gid < 0 || gid >= KSTRORAGE_MAX_GROUP_NUM) return rc; - struct list_head *head = &kstorage_groups[gid]; + struct hlist_head *bucket = kstorage_bucket(gid, did); spinlock_t *lock = &kstorage_glocks[gid]; struct kstorage *pos = 0; spin_lock(lock); - list_for_each_entry(pos, head, list) + hlist_for_each_entry_rcu(pos, bucket, hnode) { if (pos->did == did) { - list_del_rcu(&pos->list); + hlist_del_rcu(&pos->hnode); spin_unlock(lock); group_sizes[gid]--; @@ -255,7 +269,9 @@ KP_EXPORT_SYMBOL(remove_kstorage); int kstorage_init() { for (int i = 0; i < KSTRORAGE_MAX_GROUP_NUM; i++) { - INIT_LIST_HEAD(&kstorage_groups[i]); + for (int b = 0; b < KSTORAGE_NBUCKETS; b++) { + INIT_HLIST_HEAD(&kstorage_groups[i][b]); + } spin_lock_init(&kstorage_glocks[i]); } spin_lock_init(&used_max_group_lock); diff --git a/kernel/patch/include/kstorage.h b/kernel/patch/include/kstorage.h index fca8c68a..66d446cc 100644 --- a/kernel/patch/include/kstorage.h +++ b/kernel/patch/include/kstorage.h @@ -12,7 +12,7 @@ struct kstorage { - struct list_head list; + struct hlist_node hnode; struct rcu_head rcu; int gid; From 28be3847dd80e1f2a89dfc877296ede390903bb4 Mon Sep 17 00:00:00 2001 From: Mohammed Riad <1@mhmrdd.me> Date: Wed, 22 Jul 2026 23:01:24 +0100 Subject: [PATCH 2/6] fix(sucompat): hook getname_flags instead of per-syscall table The faccessat and newfstatat table hooks add a fixed cost only to those syscalls, so userspace can detect them by averaging cntvct_el0 timed samples against an unhooked path syscall. Hook the shared getname_flags path that faccessat, newfstatat, statx, openat and execve all reach, so every path syscall carries the same cost. The su path is redirected in place on the already-copied kernel name, so the callback reads no user memory and faults no page. --- kernel/patch/common/sucompat.c | 37 +++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/kernel/patch/common/sucompat.c b/kernel/patch/common/sucompat.c index fc712f74..4e3e5bf2 100644 --- a/kernel/patch/common/sucompat.c +++ b/kernel/patch/common/sucompat.c @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -310,7 +311,7 @@ __maybe_unused static void before_execveat(hook_fargs5_t *args, void *udata) // int, dfd, const char __user *, filename, unsigned, flags, // unsigned int, mask, // struct statx __user *, buffer) -static void su_handler_arg1_ufilename_before(hook_fargs6_t *args, void *udata) +__maybe_unused static void su_handler_arg1_ufilename_before(hook_fargs6_t *args, void *udata) { uid_t uid = current_uid(); if (!is_su_allow_uid(uid) && !is_trusted_manager_uid(uid)) return; @@ -331,6 +332,20 @@ static void su_handler_arg1_ufilename_before(hook_fargs6_t *args, void *udata) } } +static void after_getname_flags(hook_fargs3_t *args, void *udata) +{ + struct filename *fn = (struct filename *)args->ret; + if (IS_ERR_OR_NULL(fn)) return; + + uid_t uid = current_uid(); + if (!is_su_allow_uid(uid) && !is_trusted_manager_uid(uid)) return; + + const char *name = fn->name; + if (!name || strcmp(name, su_get_path())) return; + + if (strlen(sh_path) <= strlen(name)) strcpy((char *)name, sh_path); +} + int set_ap_mod_exclude(uid_t uid, int exclude) { int rc = 0; @@ -394,23 +409,17 @@ int su_compat_init() rc = hook_syscalln(__NR_execve, 3, before_execve, 0, (void *)0); log_boot("hook __NR_execve rc: %d\n", rc); - rc = hook_syscalln(__NR3264_fstatat, 4, su_handler_arg1_ufilename_before, 0, (void *)0); - log_boot("hook __NR3264_fstatat rc: %d\n", rc); - - rc = hook_syscalln(__NR_faccessat, 3, su_handler_arg1_ufilename_before, 0, (void *)0); - log_boot("hook __NR_faccessat rc: %d\n", rc); - // __NR_execve 11 rc = hook_compat_syscalln(11, 3, before_execve, 0, (void *)1); log_boot("hook 32 __NR_execve rc: %d\n", rc); - // __NR_fstatat64 327 - rc = hook_compat_syscalln(327, 4, su_handler_arg1_ufilename_before, 0, (void *)0); - log_boot("hook 32 __NR_fstatat64 rc: %d\n", rc); - - // __NR_faccessat 334 - rc = hook_compat_syscalln(334, 3, su_handler_arg1_ufilename_before, 0, (void *)0); - log_boot("hook 32 __NR_faccessat rc: %d\n", rc); + unsigned long getname_flags_addr = kallsyms_lookup_name("getname_flags"); + if (getname_flags_addr) { + rc = hook_wrap3((void *)getname_flags_addr, 0, after_getname_flags, (void *)0); + log_boot("hook getname_flags rc: %d\n", rc); + } else { + log_boot("getname_flags not found\n"); + } return 0; } \ No newline at end of file From 8e06e801099b584b6d68b71cb39f2414cc2abe5a Mon Sep 17 00:00:00 2001 From: Mohammed Riad <1@mhmrdd.me> Date: Wed, 22 Jul 2026 23:01:24 +0100 Subject: [PATCH 3/6] perf(hook): branch into replace target instead of RET The trampoline entry reached the replace address with a RET, but the entry is a call target, not a return edge, so RET desyncs the return address stack predictor and mispredicts on every hooked call. Use branch_absolute so the entry stays a normal branch. --- kernel/base/hook.c | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/kernel/base/hook.c b/kernel/base/hook.c index 2a823e61..08cbb943 100644 --- a/kernel/base/hook.c +++ b/kernel/base/hook.c @@ -328,16 +328,9 @@ KP_EXPORT_SYMBOL(ret_absolute); int32_t branch_from_to(uint32_t *tramp_buf, uint64_t src_addr, uint64_t dst_addr) { -#if 0 - uint32_t len = branch_relative(tramp_buf, src_addr, dst_addr); + int32_t len = branch_relative(tramp_buf, src_addr, dst_addr); if (len) return len; -#else -#if 0 - return branch_absolute(tramp_buf, dst_addr); -#else return ret_absolute(tramp_buf, dst_addr); -#endif -#endif } // transit0 @@ -558,11 +551,11 @@ hook_err_t hook_prepare(hook_t *hook) } // trampline to replace_addr if (hook->origin_insts[0] == ARM64_PACIASP || hook->origin_insts[0] == ARM64_PACIBSP) { - hook->tramp_insts_num = branch_from_to(&hook->tramp_insts[1], hook->origin_addr, hook->replace_addr); + hook->tramp_insts_num = branch_absolute(&hook->tramp_insts[1], hook->replace_addr); hook->tramp_insts[0] = ARM64_BTI_JC; hook->tramp_insts_num++; } else { - hook->tramp_insts_num = branch_from_to(hook->tramp_insts, hook->origin_addr, hook->replace_addr); + hook->tramp_insts_num = branch_absolute(hook->tramp_insts, hook->replace_addr); } // relocate From 750abdb90ff521bc532879f954b6e165c72b21bf Mon Sep 17 00:00:00 2001 From: Mohammed Riad <1@mhmrdd.me> Date: Wed, 22 Jul 2026 23:33:02 +0100 Subject: [PATCH 4/6] fix(sucompat): support sh_path longer than the su path The in-place redirect only fired when sh_path fit within the resolved su path buffer, so a shorter configured su path silently failed to redirect. Reallocate through getname_kernel and hand the new filename back via putname, staying kernel-side so no user stack buffer is written. getname_kernel and putname are a matched alloc/free pair on every supported version, so this works across 3.18 - 6.6; log when the su path still cannot be redirected. --- kernel/patch/common/sucompat.c | 16 +++++++++++++++- kernel/patch/ksyms/misc.c | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/kernel/patch/common/sucompat.c b/kernel/patch/common/sucompat.c index 4e3e5bf2..f11ac050 100644 --- a/kernel/patch/common/sucompat.c +++ b/kernel/patch/common/sucompat.c @@ -343,7 +343,21 @@ static void after_getname_flags(hook_fargs3_t *args, void *udata) const char *name = fn->name; if (!name || strcmp(name, su_get_path())) return; - if (strlen(sh_path) <= strlen(name)) strcpy((char *)name, sh_path); + if (strlen(sh_path) <= strlen(name)) { + strcpy((char *)name, sh_path); + return; + } + + if (kfunc(getname_kernel) && kfunc(putname)) { + struct filename *nf = kfunc(getname_kernel)(sh_path); + if (!IS_ERR_OR_NULL(nf)) { + kfunc(putname)(fn); + args->ret = (uint64_t)nf; + return; + } + } + + logkfi("uid: %d, cannot redirect su path to %s\n", uid, sh_path); } int set_ap_mod_exclude(uid_t uid, int exclude) diff --git a/kernel/patch/ksyms/misc.c b/kernel/patch/ksyms/misc.c index 20eeea72..a4f7baa3 100644 --- a/kernel/patch/ksyms/misc.c +++ b/kernel/patch/ksyms/misc.c @@ -467,8 +467,8 @@ static void _linux_fs_sym_match(const char *name, unsigned long addr) // kfunc_match(dentry_open, name, addr); kfunc_match(filp_close, name, addr); // kfunc_match(getname, name, addr); - // kfunc_match(getname_kernel, name, addr); - // kfunc_match(putname, name, addr); + kfunc_match(getname_kernel, name, addr); + kfunc_match(putname, name, addr); // kfunc_match(final_putname, name, addr); kfunc_match(vfs_llseek, name, addr); kfunc_match(iterate_dir, name, addr); From 0e4bb9d02839c9ad4c920ef8171bd5568ef111ac Mon Sep 17 00:00:00 2001 From: Mohammed Riad <1@mhmrdd.me> Date: Thu, 23 Jul 2026 00:31:19 +0100 Subject: [PATCH 5/6] fix(userd): gate openat and execve hooks before the user read before_openat kept copying the openat filename from userspace on every call even after its one-shot rc redirect was done, and before_execve copied the exec filename for every caller. Both let an unprivileged app detect the hook by timing and by the page it faults in. Return before the compat_strncpy_from_user: in before_openat once the redirect has run, and in before_execve for callers that are neither root nor the trusted manager. The locals are still zeroed and the auto su flag is still set first, so the after handlers and every boot-time trigger are unchanged. --- kernel/patch/android/userd.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/kernel/patch/android/userd.c b/kernel/patch/android/userd.c index 3c4c7adf..6dd4b2bc 100644 --- a/kernel/patch/android/userd.c +++ b/kernel/patch/android/userd.c @@ -1372,6 +1372,8 @@ static void handle_before_execve(hook_local_t *hook_local, char **__user u_filen hook_local->data0 = 0; } + if (current_uid() != 0 && !hook_local->data0) return; + static char app_process[] = "/system/bin/app_process"; static char app_process64[] = "/system/bin/app_process64"; static int first_app_process_execed = 0; @@ -1521,13 +1523,13 @@ static void before_openat(hook_fargs4_t *args, void *udata) args->local.data3 = 0; static int replaced = 0; + if (replaced) return; + const char __user *filename = (typeof(filename))syscall_argn(args, 1); char buf[256]; long rc = compat_strncpy_from_user(buf, filename, sizeof(buf)); if (rc <= 0) return; - if (replaced) return; - int file_count = sizeof(ORIGIN_RC_FILES) / sizeof(ORIGIN_RC_FILES[0]); for (int i = 0; i < file_count; i++) { if (ORIGIN_RC_FILES[i][0] == '\0') break; From cd52cf03cad10ec3d4f199660b273273d10955b4 Mon Sep 17 00:00:00 2001 From: Mohammed Riad <1@mhmrdd.me> Date: Thu, 23 Jul 2026 00:43:17 +0100 Subject: [PATCH 6/6] fix(userd): gate openat hook to root to close the post-boot window before_openat only self-unhooks after a rc file matches. If no match ever happens the hook stays live into app runtime, where the replaced early-return is not taken and every openat is copied from userspace, letting an app detect it by timing and residency. Return for non-root callers. The rc files are opened by init as root, so the redirect still fires, while app openat calls no longer reach the user read. --- kernel/patch/android/userd.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kernel/patch/android/userd.c b/kernel/patch/android/userd.c index 6dd4b2bc..079d9316 100644 --- a/kernel/patch/android/userd.c +++ b/kernel/patch/android/userd.c @@ -1525,6 +1525,8 @@ static void before_openat(hook_fargs4_t *args, void *udata) if (replaced) return; + if (current_uid() != 0) return; + const char __user *filename = (typeof(filename))syscall_argn(args, 1); char buf[256]; long rc = compat_strncpy_from_user(buf, filename, sizeof(buf));