又又双叒叕碰到kernel crash了

429 阅读3分钟

又又双叒叕碰到kernel crash了,kernel log里显示“Unable to handle kernel NULL pointer dereference at virtual address 00000018”,提示的很明显,访问空指针了,而且还打印出了 PC is at d_path+0x28/0x1ac 提示,大概率在调d_path函数时触发的,

//不详的征兆:难道第一个参数访问空指针了??d_path( &file->f_path, full_path, MAX_PATH );

已知出现的场景是 insmod ko时,触发crash,之前这部分代码是没问题的(Android10以上,kernel 4.0以上),为什么偏偏在该终端下出现问题(基于Android6,kernel3.10),那么只能看看是否存在什么差异了。

首先查看insmod命令实现,发现是toybox里的,Android6里的源码为如下:

//https://www.androidos.net.cn/android/6.0.1_r16/xref/external/toybox/toys/other/insmod.c 源码位置
void insmod_main(void)
{
  char * buf = NULL;
  int len, res, i;
  int fd = xopen(*toys.optargs, O_RDONLY);
​
  len = fdlength(fd);
  buf = xmalloc(len);
  xreadall(fd, buf, len);
​
  i = 1;
  while(toys.optargs[i] &&
    strlen(toybuf) + strlen(toys.optargs[i]) + 2 < sizeof(toybuf))
  {
    strcat(toybuf, toys.optargs[i++]);
    strcat(toybuf, " ");
  }
​
  res = init_module(buf, len, toybuf); //可以看到执行insmod时,会调用init_module系统调用
  if (CFG_TOYBOX_FREE) {
    if (buf != toybuf) free(buf);
    close(fd);
  }
​
  if (res) perror_exit("failed to load %s", toys.optargs[0]);
}

那么再看下kernel里的init_module实现, 

//https://elixir.bootlin.com/linux/v3.10.90/source/kernel/module.c#L3339
/* Sets info->hdr and info->len. */
static int copy_module_from_user(const void __user *umod, unsigned long len,
          struct load_info *info)
{
  int err;
​
  info->len = len;
  if (info->len < sizeof(*(info->hdr)))
    return -ENOEXEC;
//发现了端倪,这里为什么传NULL呢????这的代码根本没意义啊
  err = security_kernel_module_from_file(NULL);
  if (err)
    return err;
​
  /* Suck in entire file: we'll want most of it. */
  info->hdr = vmalloc(info->len);
  if (!info->hdr)
    return -ENOMEM;
​
  if (copy_from_user(info->hdr, umod, info->len) != 0) {
    vfree(info->hdr);
    return -EFAULT;
  }
​
  return 0;
}
​
SYSCALL_DEFINE3(init_module, void __user *, umod,
    unsigned long, len, const char __user *, uargs)
{
  int err;
  struct load_info info = { };
​
  err = may_init_module();
  if (err)
    return err;
​
  pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
         umod, len, uargs);
​
  err = copy_module_from_user(umod, len, &info);
  if (err)
    return err;
​
  return load_module(&info, uargs, 0);
}

kernel源码里 传的NULL,crash处的代码因为hook了kernel_module_from_file函数,

加上自己的代码里没有做好空指针检测,所以直接触发了crash。

查看最新6.12内核里的源码,发现已经重新实现了该部分逻辑。

//https://elixir.bootlin.com/linux/v6.12-rc6/source/kernel/module/main.c#L1961 源码位置
/* Sets info->hdr and info->len. */
static int copy_module_from_user(const void __user *umod, unsigned long len,
          struct load_info *info)
{
  int err;
​
  info->len = len;
  if (info->len < sizeof(*(info->hdr)))
    return -ENOEXEC;
​
  err = security_kernel_load_data(LOADING_MODULE, true);
  if (err)
    return err;
​
  /* Suck in entire file: we'll want most of it. */
  info->hdr = __vmalloc(info->len, GFP_KERNEL | __GFP_NOWARN);
  if (!info->hdr)
    return -ENOMEM;
​
  if (copy_chunked_from_user(info->hdr, umod, info->len) != 0) {
    err = -EFAULT;
    goto out;
  }
​
  err = security_kernel_post_load_data((char *)info->hdr, info->len,
               LOADING_MODULE, "init_module");
out:
  if (err)
    vfree(info->hdr);
​
  return err;
}

看来引入了新的逻辑,security_kernel_post_load_data ,查看下kernel commit log

git.kernel.org/pub/scm/lin…

其中提到了

With this change, LSMs can gain coverage over non-file-backed data loads (e.g. init_module(2) and firmware userspace helper), which will happen in subsequent patches.

也就是说,从引入这个新函数之后,LSM才有了对init_module non-file-backed data场景下的覆盖?

那还有个疑问,为什么之前没有出现crash呢?查看下Android10版本以上的toybox源码,发现

//https://www.androidos.net.cn/android/10.0.0_r6/xref/external/toybox/toys/other/insmod.c 源码位置
void insmod_main(void)
{
  int fd = xopenro(*toys.optargs);
  int i, rc;
​
  i = 1;
  while (toys.optargs[i] &&
    strlen(toybuf) + strlen(toys.optargs[i]) + 2 < sizeof(toybuf))
  {
    strcat(toybuf, toys.optargs[i++]);
    strcat(toybuf, " ");
  }
​
  // finit_module was new in Linux 3.8, and doesn't work on stdin,
  // so we fall back to init_module if necessary.
  //好吧,这里说的很清楚了,逻辑为会先尝试finit_module,不成功,再尝试init_module,看来之前触发的都是finit_module逻辑
  rc = finit_module(fd, toybuf, 0);
  if (rc && (fd == 0 || errno == ENOSYS)) {
    off_t len = 0;
    char *path = !strcmp(*toys.optargs, "-") ? "/dev/stdin" : *toys.optargs;
    char *buf = readfileat(AT_FDCWD, path, NULL, &len);
​
    rc = init_module(buf, len, toybuf);
    if (CFG_TOYBOX_FREE) free(buf);
  }
​
  if (rc) perror_exit("failed to load %s", toys.optargs[0]);
​
  if (CFG_TOYBOX_FREE) close(fd);
}

既然这样那就只能加上非空检测健壮自己的代码了。确实令人崩溃啊。。