From 2fd2e808c0b0d44988206dd6b233f3bd2ae5a12e Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Sun, 24 May 2026 23:01:22 +0800 Subject: [PATCH 1/6] stdio logger --- userspace/ksud/src/cli.rs | 6 ++- userspace/ksud/src/logger.rs | 94 ++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 userspace/ksud/src/logger.rs diff --git a/userspace/ksud/src/cli.rs b/userspace/ksud/src/cli.rs index 7e04c3d95088..eecc5ba2d8da 100644 --- a/userspace/ksud/src/cli.rs +++ b/userspace/ksud/src/cli.rs @@ -468,10 +468,12 @@ enum UmountOp { } pub fn run() -> Result<()> { - android_logger::init_once( + let android_max_level = crate::debug_select!(LevelFilter::Trace, LevelFilter::Info); + crate::logger::init_once( Config::default() - .with_max_level(crate::debug_select!(LevelFilter::Trace, LevelFilter::Info)) + .with_max_level(android_max_level) .with_tag("KernelSU"), + android_max_level, ); // the kernel executes su with argv[0] = "su" and replace it with us diff --git a/userspace/ksud/src/logger.rs b/userspace/ksud/src/logger.rs new file mode 100644 index 000000000000..59d4d7ef8fbe --- /dev/null +++ b/userspace/ksud/src/logger.rs @@ -0,0 +1,94 @@ +use android_logger::{AndroidLogger, Config}; +use log::{Level, LevelFilter, Log, Metadata, Record}; +use std::sync::OnceLock; +use std::sync::atomic::{AtomicUsize, Ordering}; + +static LOGGER: OnceLock = OnceLock::new(); +static ANDROID_LOG_MAX_LEVEL: AtomicUsize = AtomicUsize::new(level_filter_to_usize(LevelFilter::Off)); +static STDIO_LOG_MAX_LEVEL: AtomicUsize = AtomicUsize::new(level_filter_to_usize(LevelFilter::Off)); + +pub struct MyLogger { + android: AndroidLogger, +} + +impl MyLogger { + fn new(config: Config) -> Self { + Self { + android: AndroidLogger::new(config), + } + } + + fn println_enabled(level: Level) -> bool { + level_filter_to_usize(level.to_level_filter()) <= STDIO_LOG_MAX_LEVEL.load(Ordering::Relaxed) + } +} + +impl Log for MyLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + self.android.enabled(metadata) || Self::println_enabled(metadata.level()) + } + + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } + + if self.android.enabled(record.metadata()) { + self.android.log(record); + } + + if Self::println_enabled(record.level()) { + println!("[{}] {}", record.level(), record.args()); + } + } + + fn flush(&self) { + self.android.flush(); + } +} + +pub fn init_once(config: Config, android_max_level: LevelFilter) { + ANDROID_LOG_MAX_LEVEL.store(level_filter_to_usize(android_max_level), Ordering::Relaxed); + + let logger = LOGGER.get_or_init(|| MyLogger::new(config)); + + if let Err(err) = log::set_logger(logger) { + log::debug!("logger: log::set_logger failed: {err}"); + } else { + update_global_max_level(); + } +} + +pub fn set_stdio_log_max_level(level: LevelFilter) { + STDIO_LOG_MAX_LEVEL.store(level_filter_to_usize(level), Ordering::Relaxed); + update_global_max_level(); +} + +fn update_global_max_level() { + let max_level = ANDROID_LOG_MAX_LEVEL + .load(Ordering::Relaxed) + .max(STDIO_LOG_MAX_LEVEL.load(Ordering::Relaxed)); + log::set_max_level(level_filter_from_usize(max_level)); +} + +const fn level_filter_to_usize(level: LevelFilter) -> usize { + match level { + LevelFilter::Off => 0, + LevelFilter::Error => 1, + LevelFilter::Warn => 2, + LevelFilter::Info => 3, + LevelFilter::Debug => 4, + LevelFilter::Trace => 5, + } +} + +const fn level_filter_from_usize(level: usize) -> LevelFilter { + match level { + 0 => LevelFilter::Off, + 1 => LevelFilter::Error, + 2 => LevelFilter::Warn, + 3 => LevelFilter::Info, + 4 => LevelFilter::Debug, + _ => LevelFilter::Trace, + } +} From d54172b35edf6610c226c9ff2be01639a2978050 Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Sun, 24 May 2026 23:02:43 +0800 Subject: [PATCH 2/6] kernel: unloadable --- kernel/Kbuild | 1 + kernel/core/init.c | 16 +++- kernel/core/unload.c | 151 ++++++++++++++++++++++++++++++++++++ kernel/include/ksu.h | 1 + kernel/supercall/dispatch.c | 3 + uapi/supercall.h | 1 + 6 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 kernel/core/unload.c diff --git a/kernel/Kbuild b/kernel/Kbuild index fd31bb517040..5586896e4c2a 100644 --- a/kernel/Kbuild +++ b/kernel/Kbuild @@ -1,4 +1,5 @@ kernelsu-objs := core/init.o +kernelsu-objs += core/unload.o kernelsu-objs += feature/kernel_umount.o kernelsu-objs += feature/sulog.o diff --git a/kernel/core/init.c b/kernel/core/init.c index 8b6bd1092347..bb1d6f738117 100644 --- a/kernel/core/init.c +++ b/kernel/core/init.c @@ -80,6 +80,13 @@ bool allow_shell = false; #endif module_param(allow_shell, bool, 0); +#ifndef CONFIG_KSU_DEBUG +bool ksu_unloadable = false; +module_param_named(unloadable, ksu_unloadable, bool, 0); +#endif + +void __init remove_my_kobj(void); + int __init kernelsu_init(void) { #if defined(__x86_64__) @@ -176,7 +183,14 @@ int __init kernelsu_init(void) #ifdef MODULE #ifndef CONFIG_KSU_DEBUG - kobject_del(&THIS_MODULE->mkobj.kobj); + if (ksu_unloadable) { + pr_info("KernelSU unloadable is enabled\n"); + remove_my_kobj(); + } else { + kobject_del(&THIS_MODULE->mkobj.kobj); + // add a reference to myself to prevent from unloading + try_module_get(THIS_MODULE); + } #endif #endif return 0; diff --git a/kernel/core/unload.c b/kernel/core/unload.c new file mode 100644 index 000000000000..3c7d6f584e7f --- /dev/null +++ b/kernel/core/unload.c @@ -0,0 +1,151 @@ +#include +#include +#include +#include +#include +#include +#include "klog.h" // IWYU pragma: keep + +// we should have no usages +// module_mutex become static since 5.15, so disable this +/* +static void del_usage_links(struct module *mod) +{ + struct module_use *use; + + mutex_lock(&module_mutex); + list_for_each_entry(use, &mod->target_list, target_list) + sysfs_remove_link(use->target->holders_dir, mod->name); + mutex_unlock(&module_mutex); +}*/ + +static void __init module_remove_modinfo_attrs(struct module *mod, int end) +{ + struct module_attribute *attr; + int i; + + for (i = 0; (attr = &mod->modinfo_attrs[i]); i++) { + if (end >= 0 && i > end) + break; + /* pick a field to test for end of list */ + if (!attr->attr.name) + break; + sysfs_remove_file(&mod->mkobj.kobj, &attr->attr); + if (attr->free) + attr->free(mod); + } + kfree(mod->modinfo_attrs); + // ADDED: + mod->modinfo_attrs = kzalloc(sizeof(struct module_attribute), GFP_KERNEL); +} + +struct module_notes_attrs { + struct kobject *dir; + unsigned int notes; + struct bin_attribute attrs[]; +}; + +static void __init free_notes_attrs(struct module_notes_attrs *notes_attrs, + unsigned int i) +{ + if (notes_attrs->dir) { + while (i-- > 0) + sysfs_remove_bin_file(notes_attrs->dir, + ¬es_attrs->attrs[i]); + kobject_put(notes_attrs->dir); + } + kfree(notes_attrs); +} + +static void __init remove_notes_attrs(struct module *mod) +{ + if (mod->notes_attrs) + free_notes_attrs(mod->notes_attrs, mod->notes_attrs->notes); + // ADDED + mod->notes_attrs = NULL; +} + +struct module_sect_attr { + struct bin_attribute battr; + unsigned long address; +}; + +struct module_sect_attrs { + struct attribute_group grp; + unsigned int nsections; + struct module_sect_attr attrs[]; +}; + +static void __init free_sect_attrs(struct module_sect_attrs *sect_attrs) +{ + unsigned int section; + + for (section = 0; section < sect_attrs->nsections; section++) + kfree(sect_attrs->attrs[section].battr.attr.name); + kfree(sect_attrs); +} + +static void __init remove_sect_attrs(struct module *mod) +{ + if (mod->sect_attrs) { + sysfs_remove_group(&mod->mkobj.kobj, + &mod->sect_attrs->grp); + /* We are positive that no one is using any sect attrs + * at this point. Deallocate immediately. */ + free_sect_attrs(mod->sect_attrs); + mod->sect_attrs = NULL; + } +} + +static void mod_kobject_put(struct module *mod) +{ + DECLARE_COMPLETION_ONSTACK(c); + mod->mkobj.kobj_completion = &c; + kobject_put(&mod->mkobj.kobj); + wait_for_completion(&c); +} + +static void __init mod_sysfs_fini(struct module *mod) +{ + remove_notes_attrs(mod); + remove_sect_attrs(mod); + mod_kobject_put(mod); +} + +void my_release(struct kobject *kobj) { + struct module_kobject *mk = container_of(kobj, struct module_kobject, kobj); + + if (mk->kobj_completion) + complete(mk->kobj_completion); +} + +struct kobj_type my_type = { + .release = my_release +}; + +// copy from mod_sysfs_teardown of module.c(module/main.c) +// unstable, may panic, so it's an optional feature +void __init remove_my_kobj(void) +{ + struct module *mod = THIS_MODULE; + // del_usage_links(mod); + module_remove_modinfo_attrs(mod, -1); + module_param_sysfs_remove(mod); + kobject_put(mod->mkobj.drivers_dir); + kobject_put(mod->holders_dir); + mod_sysfs_fini(mod); + + // ADDED + pr_info("refcnt: %d state_in_sysfs: %d state_initialized: %d parent: 0x%lx\n", + mod->mkobj.kobj.kref.refcount.refs.counter, + mod->mkobj.kobj.state_in_sysfs, + mod->mkobj.kobj.state_initialized, + (unsigned long )mod->mkobj.kobj.parent + ); + mod->mkobj.kobj.state_initialized = 1; + mod->mkobj.kobj.kref.refcount.refs.counter = 1; + mod->mkobj.kobj.ktype = &my_type; + mod->mkobj.kobj.name = NULL; + mod->mkobj.kobj.parent = NULL; + mod->holders_dir = NULL; +} diff --git a/kernel/include/ksu.h b/kernel/include/ksu.h index 621096be775c..11a045a90248 100644 --- a/kernel/include/ksu.h +++ b/kernel/include/ksu.h @@ -10,6 +10,7 @@ extern struct cred *ksu_cred; extern bool ksu_late_loaded; extern bool allow_shell; +extern bool ksu_unloadable; extern struct selinux_policy *backup_sepolicy; static inline int startswith(char *s, char *prefix) diff --git a/kernel/supercall/dispatch.c b/kernel/supercall/dispatch.c index 1990b5d146b6..1007ef9ce0e2 100644 --- a/kernel/supercall/dispatch.c +++ b/kernel/supercall/dispatch.c @@ -51,6 +51,9 @@ static int do_get_info(void __user *arg) if (ksu_late_loaded) { cmd.flags |= KSU_GET_INFO_FLAG_LATE_LOAD; } + if (ksu_unloadable) { + cmd.flags |= KSU_GET_INFO_FLAG_UNLOADABLE; + } #ifdef EXPECTED_SIZE2 cmd.flags |= KSU_GET_INFO_FLAG_PR_BUILD; #endif diff --git a/uapi/supercall.h b/uapi/supercall.h index cae5da2fa821..312de291a23d 100644 --- a/uapi/supercall.h +++ b/uapi/supercall.h @@ -22,6 +22,7 @@ static const __u32 KSU_GET_INFO_FLAG_LKM = (1U << 0); static const __u32 KSU_GET_INFO_FLAG_MANAGER = (1U << 1); static const __u32 KSU_GET_INFO_FLAG_LATE_LOAD = (1U << 2); static const __u32 KSU_GET_INFO_FLAG_PR_BUILD = (1U << 3); +static const __u32 KSU_GET_INFO_FLAG_UNLOADABLE = (1U << 4); struct ksu_get_info_cmd { __u32 version; /* Output: KERNEL_SU_VERSION */ From 7b55b51f27bd23c61d49271cd3bc0e03c0c20cfc Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Sun, 24 May 2026 23:03:05 +0800 Subject: [PATCH 3/6] userspace: support unload (WIP) --- userspace/ksud/src/boot_patch.rs | 22 +++++++++++++ userspace/ksud/src/ksucalls.rs | 2 +- userspace/ksud/src/main.rs | 2 ++ userspace/ksud/src/unload.rs | 56 +++++++------------------------- userspace/ksuinit/src/init.rs | 16 ++++++--- 5 files changed, 49 insertions(+), 49 deletions(-) diff --git a/userspace/ksud/src/boot_patch.rs b/userspace/ksud/src/boot_patch.rs index 8b24b31c6676..47ebbc60171d 100644 --- a/userspace/ksud/src/boot_patch.rs +++ b/userspace/ksud/src/boot_patch.rs @@ -526,6 +526,10 @@ pub struct BootPatchArgs { /// Do not (re-)install kernelsu, only modify configs (allow_shell, etc.) #[arg(long, default_value = "false")] no_install: bool, + + /// Allow unload KernelSU LKM at runtime + #[arg(long, default_value = "false")] + unloadable: bool, } pub fn patch(args: BootPatchArgs) -> Result<()> { @@ -544,6 +548,7 @@ pub fn patch(args: BootPatchArgs) -> Result<()> { enable_adbd, adb_debug_prop, no_install, + unloadable, #[cfg(target_os = "android")] ota, #[cfg(target_os = "android")] @@ -773,6 +778,23 @@ pub fn patch(args: BootPatchArgs) -> Result<()> { } } + if unloadable { + println!("- Adding unloadable config"); + { + let unloadable_file = workdir.join("ksu_unloadable"); + File::create(unloadable_file)?; + } + do_cpio_cmd( + &magiskboot, + workdir, + ramdisk, + "add 0644 ksu_unloadable ksu_unloadable", + )?; + } else if do_cpio_cmd(&magiskboot, workdir, ramdisk, "exists ksu_unloadable").is_ok() { + println!("- Removing unloadable config"); + do_cpio_cmd(&magiskboot, workdir, ramdisk, "rm ksu_unloadable").ok(); + } + println!("- Repacking boot image"); // magiskboot repack boot.img let status = Command::new(&magiskboot) diff --git a/userspace/ksud/src/ksucalls.rs b/userspace/ksud/src/ksucalls.rs index b85b64b7fe48..dee77301179d 100644 --- a/userspace/ksud/src/ksucalls.rs +++ b/userspace/ksud/src/ksucalls.rs @@ -62,7 +62,7 @@ fn ksuctl(request: u32, arg: *mut T) -> std::io::Result { } // API implementations -fn get_info() -> ksu_uapi::ksu_get_info_cmd { +pub fn get_info() -> ksu_uapi::ksu_get_info_cmd { *INFO_CACHE.get_or_init(|| { let mut cmd = ksu_uapi::ksu_get_info_cmd { version: 0, diff --git a/userspace/ksud/src/main.rs b/userspace/ksud/src/main.rs index 7d7acd0ecaa0..87ac0c94b723 100644 --- a/userspace/ksud/src/main.rs +++ b/userspace/ksud/src/main.rs @@ -29,6 +29,8 @@ mod ksucalls; #[cfg(target_os = "android")] mod late_load; #[cfg(target_os = "android")] +mod logger; +#[cfg(target_os = "android")] mod magica; #[cfg(target_os = "android")] mod metamodule; diff --git a/userspace/ksud/src/unload.rs b/userspace/ksud/src/unload.rs index 7a386045dbc1..275f0c4c13fe 100644 --- a/userspace/ksud/src/unload.rs +++ b/userspace/ksud/src/unload.rs @@ -1,40 +1,9 @@ use anyhow::Result; -use log::{info, warn}; +use log::{error, info, warn}; use std::fs; use std::process::Command; -use crate::utils; - -/// Find PIDs of processes running in the KernelSU su domain (u:r:ksu:s0). -/// Returns a list of PIDs excluding our own. -fn find_su_domain_pids() -> Vec { - let my_pid = std::process::id() as i32; - let mut pids = Vec::new(); - - let Ok(entries) = fs::read_dir("/proc") else { - return pids; - }; - - for entry in entries.flatten() { - let name = entry.file_name(); - let Some(pid) = name.to_str().and_then(|s| s.parse::().ok()) else { - continue; - }; - if pid == my_pid { - continue; - } - - let attr_path = format!("/proc/{pid}/attr/current"); - if let Ok(context) = fs::read_to_string(&attr_path) { - let context = context.trim().trim_end_matches('\0'); - if context == "u:r:ksu:s0" { - pids.push(pid); - } - } - } - - pids -} +use crate::{ksu_uapi, ksucalls, logger::set_stdio_log_max_level, utils}; /// Find PIDs of processes holding ksu_driver or ksu_fdwrapper file descriptors. /// Returns a list of PIDs excluding our own. @@ -106,8 +75,16 @@ fn close_ksu_fds() { } pub fn unload() -> Result<()> { + set_stdio_log_max_level(log::LevelFilter::Info); + if (ksucalls::get_info().flags & ksu_uapi::KSU_GET_INFO_FLAG_UNLOADABLE) == 0 { + error!("KernelSU is not unloadable!"); + std::process::exit(1); + } + info!("unload: starting KernelSU unload sequence"); + // TODO: check if we are using ksu fdwrapper + // 0. Switch cgroups so we don't get killed along with our parent shell utils::switch_cgroups(); @@ -115,17 +92,8 @@ pub fn unload() -> Result<()> { info!("unload: stopping Android services..."); let _ = Command::new("stop").status(); - // 2. Kill all su domain processes and processes holding ksu fds (except ourselves) - info!("unload: killing su domain processes..."); - let su_pids = find_su_domain_pids(); - if !su_pids.is_empty() { - info!( - "unload: found {} su domain processes, sending SIGKILL", - su_pids.len() - ); - kill_pids(&su_pids, libc::SIGKILL); - } - + // 2. Kill all processes holding ksu fds (except ourselves) + // TODO: scan and kill in kernel info!("unload: killing processes holding ksu fds..."); let fd_pids = find_ksu_fd_holders(); if !fd_pids.is_empty() { diff --git a/userspace/ksuinit/src/init.rs b/userspace/ksuinit/src/init.rs index d66a70a3d270..bef12edb551d 100644 --- a/userspace/ksuinit/src/init.rs +++ b/userspace/ksuinit/src/init.rs @@ -135,11 +135,19 @@ pub fn init() -> Result<()> { fn load_module_from_path(path: &str) -> Result<()> { anyhow::ensure!(rustix::process::getpid().is_init(), "Invalid process"); let buffer = std::fs::read(path).with_context(|| format!("Cannot read file {}", path))?; - let params = if std::fs::exists("/ksu_allow_shell").unwrap_or(false) { + let allow_shell = std::fs::exists("/ksu_allow_shell").unwrap_or(false); + let unloadable = std::fs::exists("/ksu_unloadable").unwrap_or(false); + if allow_shell { log::warn!("ksu allow shell at init!"); - cstr!("allow_shell=1") - } else { - cstr!("") + } + if unloadable { + log::warn!("ksu unloadable is enabled!"); + } + let params = match (allow_shell, unloadable) { + (true, true) => cstr!("allow_shell=1 unloadable=1"), + (true, false) => cstr!("allow_shell=1"), + (false, true) => cstr!("unloadable=1"), + (false, false) => cstr!(""), }; ksuinit::load_module(&buffer, params) } From 03ce92cd35badac4bf783fc938d17c4b7062cadb Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Sun, 24 May 2026 23:32:23 +0800 Subject: [PATCH 4/6] check fd before unload --- kernel/core/unload.c | 127 ++++++++++++++++----------------- kernel/infra/file_wrapper.c | 24 +++++++ kernel/infra/file_wrapper.h | 2 + kernel/supercall/dispatch.c | 18 +++++ uapi/supercall.h | 6 ++ userspace/ksud/src/ksucalls.rs | 6 ++ userspace/ksud/src/logger.rs | 6 +- userspace/ksud/src/unload.rs | 10 ++- 8 files changed, 128 insertions(+), 71 deletions(-) diff --git a/kernel/core/unload.c b/kernel/core/unload.c index 3c7d6f584e7f..a32826a9d749 100644 --- a/kernel/core/unload.c +++ b/kernel/core/unload.c @@ -21,127 +21,120 @@ static void del_usage_links(struct module *mod) static void __init module_remove_modinfo_attrs(struct module *mod, int end) { - struct module_attribute *attr; - int i; - - for (i = 0; (attr = &mod->modinfo_attrs[i]); i++) { - if (end >= 0 && i > end) - break; - /* pick a field to test for end of list */ - if (!attr->attr.name) - break; - sysfs_remove_file(&mod->mkobj.kobj, &attr->attr); - if (attr->free) - attr->free(mod); - } - kfree(mod->modinfo_attrs); + struct module_attribute *attr; + int i; + + for (i = 0; (attr = &mod->modinfo_attrs[i]); i++) { + if (end >= 0 && i > end) + break; + /* pick a field to test for end of list */ + if (!attr->attr.name) + break; + sysfs_remove_file(&mod->mkobj.kobj, &attr->attr); + if (attr->free) + attr->free(mod); + } + kfree(mod->modinfo_attrs); // ADDED: mod->modinfo_attrs = kzalloc(sizeof(struct module_attribute), GFP_KERNEL); } struct module_notes_attrs { - struct kobject *dir; - unsigned int notes; - struct bin_attribute attrs[]; + struct kobject *dir; + unsigned int notes; + struct bin_attribute attrs[]; }; -static void __init free_notes_attrs(struct module_notes_attrs *notes_attrs, - unsigned int i) +static void __init free_notes_attrs(struct module_notes_attrs *notes_attrs, unsigned int i) { - if (notes_attrs->dir) { - while (i-- > 0) - sysfs_remove_bin_file(notes_attrs->dir, - ¬es_attrs->attrs[i]); - kobject_put(notes_attrs->dir); - } - kfree(notes_attrs); + if (notes_attrs->dir) { + while (i-- > 0) + sysfs_remove_bin_file(notes_attrs->dir, ¬es_attrs->attrs[i]); + kobject_put(notes_attrs->dir); + } + kfree(notes_attrs); } static void __init remove_notes_attrs(struct module *mod) { - if (mod->notes_attrs) - free_notes_attrs(mod->notes_attrs, mod->notes_attrs->notes); + if (mod->notes_attrs) + free_notes_attrs(mod->notes_attrs, mod->notes_attrs->notes); // ADDED mod->notes_attrs = NULL; } struct module_sect_attr { - struct bin_attribute battr; - unsigned long address; + struct bin_attribute battr; + unsigned long address; }; struct module_sect_attrs { - struct attribute_group grp; - unsigned int nsections; - struct module_sect_attr attrs[]; + struct attribute_group grp; + unsigned int nsections; + struct module_sect_attr attrs[]; }; static void __init free_sect_attrs(struct module_sect_attrs *sect_attrs) { - unsigned int section; + unsigned int section; - for (section = 0; section < sect_attrs->nsections; section++) - kfree(sect_attrs->attrs[section].battr.attr.name); - kfree(sect_attrs); + for (section = 0; section < sect_attrs->nsections; section++) + kfree(sect_attrs->attrs[section].battr.attr.name); + kfree(sect_attrs); } static void __init remove_sect_attrs(struct module *mod) { - if (mod->sect_attrs) { - sysfs_remove_group(&mod->mkobj.kobj, - &mod->sect_attrs->grp); - /* We are positive that no one is using any sect attrs + if (mod->sect_attrs) { + sysfs_remove_group(&mod->mkobj.kobj, &mod->sect_attrs->grp); + /* We are positive that no one is using any sect attrs * at this point. Deallocate immediately. */ - free_sect_attrs(mod->sect_attrs); - mod->sect_attrs = NULL; - } + free_sect_attrs(mod->sect_attrs); + mod->sect_attrs = NULL; + } } static void mod_kobject_put(struct module *mod) { - DECLARE_COMPLETION_ONSTACK(c); - mod->mkobj.kobj_completion = &c; - kobject_put(&mod->mkobj.kobj); - wait_for_completion(&c); + DECLARE_COMPLETION_ONSTACK(c); + mod->mkobj.kobj_completion = &c; + kobject_put(&mod->mkobj.kobj); + wait_for_completion(&c); } static void __init mod_sysfs_fini(struct module *mod) { - remove_notes_attrs(mod); - remove_sect_attrs(mod); - mod_kobject_put(mod); + remove_notes_attrs(mod); + remove_sect_attrs(mod); + mod_kobject_put(mod); } -void my_release(struct kobject *kobj) { +void my_release(struct kobject *kobj) +{ struct module_kobject *mk = container_of(kobj, struct module_kobject, kobj); if (mk->kobj_completion) complete(mk->kobj_completion); } -struct kobj_type my_type = { - .release = my_release -}; +struct kobj_type my_type = { .release = my_release }; // copy from mod_sysfs_teardown of module.c(module/main.c) // unstable, may panic, so it's an optional feature void __init remove_my_kobj(void) { struct module *mod = THIS_MODULE; - // del_usage_links(mod); - module_remove_modinfo_attrs(mod, -1); - module_param_sysfs_remove(mod); - kobject_put(mod->mkobj.drivers_dir); - kobject_put(mod->holders_dir); - mod_sysfs_fini(mod); + // del_usage_links(mod); + module_remove_modinfo_attrs(mod, -1); + module_param_sysfs_remove(mod); + kobject_put(mod->mkobj.drivers_dir); + kobject_put(mod->holders_dir); + mod_sysfs_fini(mod); // ADDED - pr_info("refcnt: %d state_in_sysfs: %d state_initialized: %d parent: 0x%lx\n", - mod->mkobj.kobj.kref.refcount.refs.counter, - mod->mkobj.kobj.state_in_sysfs, - mod->mkobj.kobj.state_initialized, - (unsigned long )mod->mkobj.kobj.parent - ); + pr_info("refcnt: %d state_in_sysfs: %d state_initialized: %d parent: 0x%lx\n", + mod->mkobj.kobj.kref.refcount.refs.counter, mod->mkobj.kobj.state_in_sysfs, + mod->mkobj.kobj.state_initialized, (unsigned long)mod->mkobj.kobj.parent); mod->mkobj.kobj.state_initialized = 1; mod->mkobj.kobj.kref.refcount.refs.counter = 1; mod->mkobj.kobj.ktype = &my_type; diff --git a/kernel/infra/file_wrapper.c b/kernel/infra/file_wrapper.c index 66ea295812c1..c8e72942e7bf 100644 --- a/kernel/infra/file_wrapper.c +++ b/kernel/infra/file_wrapper.c @@ -489,6 +489,23 @@ static struct file *ksu_anon_inode_create_getfile_compat(const char *name, const } #endif +static inline bool is_wrapper_file(struct file *f) +{ + return f->f_op->release == ksu_wrapper_release; +} + +int is_wrapper_fd(int fd) +{ + int ret; + struct file *orig_file = fget(fd); + if (!orig_file) { + return -EBADF; + } + ret = is_wrapper_file(orig_file) ? 1 : 0; + fput(orig_file); + return ret; +} + int ksu_install_file_wrapper(int fd) { int out_fd, ret; @@ -497,6 +514,13 @@ int ksu_install_file_wrapper(int fd) return -EBADF; } + if (is_wrapper_file(orig_file)) { + // DEBUG + pr_info("fd %d is already a wrapper\n", fd); + ret = fd; + goto done; + } + out_fd = get_unused_fd_flags(O_CLOEXEC); if (out_fd < 0) { ret = out_fd; diff --git a/kernel/infra/file_wrapper.h b/kernel/infra/file_wrapper.h index faae4dded301..0a65460e1bea 100644 --- a/kernel/infra/file_wrapper.h +++ b/kernel/infra/file_wrapper.h @@ -7,4 +7,6 @@ int ksu_install_file_wrapper(int fd); void ksu_file_wrapper_init(void); +int is_wrapper_fd(int fd); + #endif // KSU_FILE_WRAPPER_H diff --git a/kernel/supercall/dispatch.c b/kernel/supercall/dispatch.c index 1007ef9ce0e2..79fe65f695b0 100644 --- a/kernel/supercall/dispatch.c +++ b/kernel/supercall/dispatch.c @@ -657,6 +657,18 @@ static int do_get_sulog_fd(void __user *arg) return ksu_install_sulog_fd(); } +static int do_check_wrapper_fd(void __user *arg) +{ + struct ksu_check_wrapper_fd_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("get_sulog_fd: copy_from_user failed\n"); + return -EFAULT; + } + + return is_wrapper_fd(cmd.fd); +} + // IOCTL handlers mapping table // clang-format off static const struct ksu_ioctl_cmd_map ksu_ioctl_handlers[] = { @@ -792,6 +804,12 @@ static const struct ksu_ioctl_cmd_map ksu_ioctl_handlers[] = { .handler = do_get_sulog_fd, .perm_check = only_root }, + { + .cmd = KSU_IOCTL_CHECK_WRAPPER_FD, + .name = "CHECK_WRAPPER_FD", + .handler = do_check_wrapper_fd, + .perm_check = manager_or_root + }, { .cmd = 0, .name = NULL, diff --git a/uapi/supercall.h b/uapi/supercall.h index 312de291a23d..011f8999fdc9 100644 --- a/uapi/supercall.h +++ b/uapi/supercall.h @@ -135,6 +135,10 @@ struct ksu_get_sulog_fd_cmd { __u32 flags; /* Input: reserved for future use, must be 0 */ }; +struct ksu_check_wrapper_fd_cmd { + __u32 fd; +}; + static const __u8 KSU_UMOUNT_WIPE = 0; /* ignore everything and wipe list */ static const __u8 KSU_UMOUNT_ADD = 1; /* add entry (path + flags) */ static const __u8 KSU_UMOUNT_DEL = 2; /* delete entry, strcmp */ @@ -164,5 +168,7 @@ static const __u32 KSU_IOCTL_NUKE_EXT4_SYSFS = _IOC(_IOC_WRITE, 'K', 17, 0); static const __u32 KSU_IOCTL_ADD_TRY_UMOUNT = _IOC(_IOC_WRITE, 'K', 18, 0); static const __u32 KSU_IOCTL_SET_INIT_PGRP = _IO('K', 19); static const __u32 KSU_IOCTL_GET_SULOG_FD = _IOW('K', 20, struct ksu_get_sulog_fd_cmd); +static const __u32 KSU_IOCTL_CHECK_WRAPPER_FD = _IOW('K', 21, struct ksu_check_wrapper_fd_cmd); +static const __u32 KSU_IOCTL_KILL_ALL_WRAPPER_FD_HOLDERS = _IO('K', 22); #endif diff --git a/userspace/ksud/src/ksucalls.rs b/userspace/ksud/src/ksucalls.rs index dee77301179d..05e1220a721e 100644 --- a/userspace/ksud/src/ksucalls.rs +++ b/userspace/ksud/src/ksucalls.rs @@ -153,6 +153,12 @@ pub fn get_sulog_fd() -> std::io::Result { Ok(result) } +pub fn check_wrapper_fd(fd: RawFd) -> std::io::Result { + let mut cmd = ksu_uapi::ksu_check_wrapper_fd_cmd { fd: fd as u32 }; + let result = ksuctl(ksu_uapi::KSU_IOCTL_CHECK_WRAPPER_FD, &raw mut cmd)?; + Ok(result == 1) +} + /// Get mark status for a process (pid=0 returns total marked count) pub fn mark_get(pid: i32) -> std::io::Result { let mut cmd = ksu_uapi::ksu_manage_mark_cmd { diff --git a/userspace/ksud/src/logger.rs b/userspace/ksud/src/logger.rs index 59d4d7ef8fbe..0d7e006a8c4f 100644 --- a/userspace/ksud/src/logger.rs +++ b/userspace/ksud/src/logger.rs @@ -4,7 +4,8 @@ use std::sync::OnceLock; use std::sync::atomic::{AtomicUsize, Ordering}; static LOGGER: OnceLock = OnceLock::new(); -static ANDROID_LOG_MAX_LEVEL: AtomicUsize = AtomicUsize::new(level_filter_to_usize(LevelFilter::Off)); +static ANDROID_LOG_MAX_LEVEL: AtomicUsize = + AtomicUsize::new(level_filter_to_usize(LevelFilter::Off)); static STDIO_LOG_MAX_LEVEL: AtomicUsize = AtomicUsize::new(level_filter_to_usize(LevelFilter::Off)); pub struct MyLogger { @@ -19,7 +20,8 @@ impl MyLogger { } fn println_enabled(level: Level) -> bool { - level_filter_to_usize(level.to_level_filter()) <= STDIO_LOG_MAX_LEVEL.load(Ordering::Relaxed) + level_filter_to_usize(level.to_level_filter()) + <= STDIO_LOG_MAX_LEVEL.load(Ordering::Relaxed) } } diff --git a/userspace/ksud/src/unload.rs b/userspace/ksud/src/unload.rs index 275f0c4c13fe..9a4955d311d3 100644 --- a/userspace/ksud/src/unload.rs +++ b/userspace/ksud/src/unload.rs @@ -81,9 +81,15 @@ pub fn unload() -> Result<()> { std::process::exit(1); } - info!("unload: starting KernelSU unload sequence"); + if ksucalls::check_wrapper_fd(0).is_ok_and(|x| x) + || ksucalls::check_wrapper_fd(1).is_ok_and(|x| x) + || ksucalls::check_wrapper_fd(2).is_ok_and(|x| x) + { + error!("Please restart root shell with -W!"); + std::process::exit(1); + } - // TODO: check if we are using ksu fdwrapper + info!("unload: starting KernelSU unload sequence"); // 0. Switch cgroups so we don't get killed along with our parent shell utils::switch_cgroups(); From ba0b44f7999cecd6e8c6d7adf19cdf959d3c6b72 Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Mon, 25 May 2026 00:00:56 +0800 Subject: [PATCH 5/6] add debug info --- userspace/ksud/src/cli.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/userspace/ksud/src/cli.rs b/userspace/ksud/src/cli.rs index eecc5ba2d8da..40a72dac3680 100644 --- a/userspace/ksud/src/cli.rs +++ b/userspace/ksud/src/cli.rs @@ -7,7 +7,8 @@ use log::{LevelFilter, error, info}; use crate::boot_patch::{BootPatchArgs, BootRestoreArgs}; use crate::{ - apk_sign, assets, debug, defs, init_event, ksucalls, module, module_config, sulog, utils, + apk_sign, assets, debug, defs, init_event, ksu_uapi, ksucalls, module, module_config, sulog, + utils, }; /// KernelSU userspace cli @@ -214,6 +215,9 @@ enum Debug { /// Launch sulogd daemon manually Sulogd, + + /// Get kernel info + Info, } #[derive(clap::Subcommand, Debug)] @@ -702,6 +706,29 @@ pub fn run() -> Result<()> { MarkCommand::Refresh => debug::mark_refresh(), }, Debug::Sulogd => sulog::ensure_sulogd_running(), + Debug::Info => { + let info = ksucalls::get_info(); + println!("version: {}", info.version); + println!("flags: 0x{:x}", info.flags); + println!("features: 0x{:x}", info.features); + println!( + "lkm: {}", + (info.flags & ksu_uapi::KSU_GET_INFO_FLAG_LKM) != 0 + ); + println!( + "late_load: {}", + (info.flags & ksu_uapi::KSU_GET_INFO_FLAG_LATE_LOAD) != 0 + ); + println!( + "pr_build: {}", + (info.flags & ksu_uapi::KSU_GET_INFO_FLAG_PR_BUILD) != 0 + ); + println!( + "unloadable: {}", + (info.flags & ksu_uapi::KSU_GET_INFO_FLAG_UNLOADABLE) != 0 + ); + Ok(()) + } }, Commands::BootPatch(boot_patch) => crate::boot_patch::patch(boot_patch), From d973ef033e627e649ba83891ad5cbe1481582dbe Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Mon, 25 May 2026 00:38:28 +0800 Subject: [PATCH 6/6] fix unload fsnotify --- kernel/manager/pkg_observer.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/kernel/manager/pkg_observer.c b/kernel/manager/pkg_observer.c index 512b99296fea..2e1c7045c942 100644 --- a/kernel/manager/pkg_observer.c +++ b/kernel/manager/pkg_observer.c @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0 +#include "linux/completion.h" #include #include #include @@ -35,8 +36,25 @@ static int ksu_handle_inode_event(struct fsnotify_mark *mark, u32 mask, struct i return 0; } +static void my_free_mark(struct fsnotify_mark *mark) +{ + kfree(mark); +} + +DECLARE_COMPLETION(free_group_completion); + +static void __naked my_free_group_priv(struct fsnotify_group *group) +{ + asm(".extern complete\n" + ".extern free_group_completion\n" + "adr x0, free_group_completion\n" + "b complete\n"); +} + static const struct fsnotify_ops ksu_ops = { .handle_inode_event = ksu_handle_inode_event, + .free_mark = my_free_mark, + .free_group_priv = my_free_group_priv, }; static int add_mark_on_inode(struct inode *inode, u32 mask, struct fsnotify_mark **out) @@ -120,5 +138,7 @@ void __exit ksu_observer_exit(void) { unwatch_one_dir(&g_watch); fsnotify_put_group(g); + pr_info("waiting for observer exit\n"); + wait_for_completion(&free_group_completion); pr_info("observer exit done\n"); } \ No newline at end of file