You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Linux Kernel 4.14.147(Android)系统调用钩子中kernel_write触发内核崩溃问题求助

系统调用钩子中kernel_write崩溃的排查与解决

我来帮你拆解下这个问题的核心原因,再给你落地的解决思路和代码示例:

崩溃的关键原因分析

结合你的代码和崩溃日志,问题主要出在这几个地方:

1. 无限递归调用引发栈溢出/死锁

openat是Linux里被高频调用的系统调用,kernel_write内部的文件系统操作大概率会再次触发openat(比如更新文件元数据、回写页缓存的时候)。而你的钩子函数没有做递归防护,导致new_sys_openat被无限递归调用,最终把栈撑爆或者引发死锁——看崩溃日志里的logd.klogdlmkd这些系统进程,就是它们的openat调用触发hook后,执行kernel_write又递归触发了hook,直接崩了。

2. 全局文件指针的并发竞态问题

你定义的static struct file *fp是全局变量,多个进程同时调用openat时,会同时操作这个指针:

  • 多个线程乱改fp->f_pos会导致日志写入位置完全错乱;
  • 无锁情况下并发执行kernel_writefile_sync,会直接破坏文件对象的内部状态,触发崩溃。

3. 没检查filp_open的返回值

如果/data/local/tmp还没挂载、或者内核没有权限打开这个文件,filp_open会返回错误指针,这时候fpNULL,直接调用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,跳过logdlmkdinit这些进程的日志,进一步减少冲突;
  • 避免在危险上下文执行IO:系统调用钩子是进程上下文,但工作队列是更安全的异步处理方式,能避开很多潜在问题。

内容的提问来源于stack exchange,提问作者Ronny

火山引擎 最新活动