Linux Kernel 4.14.147(Android)系统调用钩子中kernel_write触发内核崩溃问题求助
系统调用钩子中kernel_write崩溃的排查与解决
我来帮你拆解下这个问题的核心原因,再给你落地的解决思路和代码示例:
崩溃的关键原因分析
结合你的代码和崩溃日志,问题主要出在这几个地方:
1. 无限递归调用引发栈溢出/死锁
openat是Linux里被高频调用的系统调用,kernel_write内部的文件系统操作大概率会再次触发openat(比如更新文件元数据、回写页缓存的时候)。而你的钩子函数没有做递归防护,导致new_sys_openat被无限递归调用,最终把栈撑爆或者引发死锁——看崩溃日志里的logd.klogd、lmkd这些系统进程,就是它们的openat调用触发hook后,执行kernel_write又递归触发了hook,直接崩了。
2. 全局文件指针的并发竞态问题
你定义的static struct file *fp是全局变量,多个进程同时调用openat时,会同时操作这个指针:
- 多个线程乱改
fp->f_pos会导致日志写入位置完全错乱; - 无锁情况下并发执行
kernel_write和file_sync,会直接破坏文件对象的内部状态,触发崩溃。
3. 没检查filp_open的返回值
如果/data/local/tmp还没挂载、或者内核没有权限打开这个文件,filp_open会返回错误指针,这时候fp是NULL,直接调用kernel_write就会触发空指针解引用崩溃。
可行的解决方法
首选方案:用工作队列异步写日志
把日志写入操作从系统调用钩子的上下文转移到后台工作线程,从根源上避免递归调用和上下文冲突,这也是内核中处理异步IO的标准做法:
#include <linux/workqueue.h> #include <linux/slab.h> #include <linux/mutex.h> // 定义日志结构体,保存要写入的内容 struct log_entry { char msg[1100]; struct work_struct work; }; static struct file *fp = NULL; static DEFINE_MUTEX(fp_mutex); // 保护fp的互斥锁 static struct workqueue_struct *log_wq; // 专属工作队列 // 工作队列处理函数:实际执行文件写入 static void write_log_work(struct work_struct *work) { struct log_entry *entry = container_of(work, struct log_entry, work); loff_t pos; int len = strlen(entry->msg); mutex_lock(&fp_mutex); if (fp) { pos = fp->f_pos; kernel_write(fp, entry->msg, len, &pos); fp->f_pos = pos; vfs_fsync(fp, 0); } mutex_unlock(&fp_mutex); kfree(entry); } asmlinkage long new_sys_openat(unsigned int dfd, const char __user *filename, int flags, umode_t mode) { struct log_entry *entry; char fname[NAME_MAX] = {0}; // 递归防护:如果当前是工作队列线程,直接跳过日志写入 if (current->flags & PF_WQ_WORKER) { return real_sys_openat(dfd, filename, flags, mode); } // 拷贝用户空间的文件名 if (strncpy_from_user(fname, filename, NAME_MAX) < 0) { goto out; } // 分配日志条目内存 entry = kmalloc(sizeof(struct log_entry), GFP_KERNEL); if (!entry) { goto out; } // 格式化日志内容 snprintf(entry->msg, sizeof(entry->msg), "openat(\"%s\", %hd)\n", fname, mode); INIT_WORK(&entry->work, write_log_work); // 将工作项加入队列 queue_work(log_wq, &entry->work); out: return real_sys_openat(dfd, filename, flags, mode); } static int __init konon() { // 创建单线程工作队列,保证日志顺序 log_wq = create_singlethread_workqueue("syscall_log_wq"); if (!log_wq) { return -ENOMEM; } mutex_lock(&fp_mutex); // 打开日志文件,添加O_APPEND保证追加写入 fp = filp_open("/data/local/tmp/openat64.log", O_CREAT|O_WRONLY|O_LARGEFILE|O_APPEND, 0666); // 检查文件是否打开成功 if (IS_ERR(fp)) { fp = NULL; mutex_unlock(&fp_mutex); destroy_workqueue(log_wq); return PTR_ERR(fp); } mutex_unlock(&fp_mutex); // 这里放你的系统调用表替换代码... return 0; } static void __exit konoff() { // 等待所有未完成的工作项执行完毕 flush_workqueue(log_wq); destroy_workqueue(log_wq); mutex_lock(&fp_mutex); if (fp) { filp_close(fp, NULL); fp = NULL; } mutex_unlock(&fp_mutex); // 这里放你的系统调用表恢复代码... } module_init(konon); module_exit(konoff);
备选方案:添加递归防护与并发锁
如果一定要在钩子上下文同步写日志,至少要加上递归检查和互斥锁,避免竞态和递归:
#include <linux/mutex.h> static struct file *fp = NULL; static DEFINE_MUTEX(fp_mutex); static bool is_writing = false; // 标记是否正在写日志,防止递归 asmlinkage long new_sys_openat(unsigned int dfd, const char __user *filename, int flags, umode_t mode) { int len; char buff[1100] = {0}, fname[NAME_MAX] = {0}; loff_t pos; // 递归防护:如果正在写日志,直接跳过 if (is_writing) { return real_sys_openat(dfd, filename, flags, mode); } // 拷贝用户空间文件名 if (strncpy_from_user(fname, filename, NAME_MAX) < 0) { goto out; } len = snprintf(buff, sizeof(buff), "openat(\"%s\", %hd)\n", fname, mode); mutex_lock(&fp_mutex); if (fp) { is_writing = true; pos = fp->f_pos; kernel_write(fp, buff, len, &pos); fp->f_pos = pos; vfs_fsync(fp, 0); is_writing = false; } mutex_unlock(&fp_mutex); out: return real_sys_openat(dfd, filename, flags, mode); } static int __init konon() { mutex_lock(&fp_mutex); fp = filp_open("/data/local/tmp/openat64.log", O_CREAT|O_WRONLY|O_LARGEFILE|O_APPEND, 0666); if (IS_ERR(fp)) { fp = NULL; mutex_unlock(&fp_mutex); return PTR_ERR(fp); } mutex_unlock(&fp_mutex); // 系统调用表替换代码... return 0; } static void __exit konoff() { mutex_lock(&fp_mutex); if (fp) { filp_close(fp, NULL); fp = NULL; } mutex_unlock(&fp_mutex); // 系统调用表恢复代码... } module_init(konon); module_exit(konoff);
额外注意事项
- 一定要检查
filp_open的返回值:用IS_ERR(fp)判断是否打开失败,绝对不能忽略; - 添加
O_APPEND标志:即使有锁,这个标志也能保证日志是按顺序追加的,避免位置错乱; - 过滤系统关键进程:可以通过
current->comm(进程名)或者current->pid,跳过logd、lmkd、init这些进程的日志,进一步减少冲突; - 避免在危险上下文执行IO:系统调用钩子是进程上下文,但工作队列是更安全的异步处理方式,能避开很多潜在问题。
内容的提问来源于stack exchange,提问作者Ronny




