3

我正在linux中实现我自己的系统调用。它在其中调用重命名系统调用。它使用用户参数(下面是代码)将代码传递给重命名。

这是基本代码:

int sys_mycall(const char __user * inputFile)   {

//
// Code to generate my the "fileName"
//
//

old_fs = get_fs();
set_fs(KERNEL_DS);

    ans =  sys_renameat(AT_FDCWD, fileName, AT_FDCWD, inputFile);

set_fs(old_fs);

    return ans;

}

我在这里有两个疑问。

  1. 我正在使用old_fs = get_fs();,set_fs(KERNEL_DS);set_fs(old_fs);绕过实际调用,sys_rename因为出现错误。我从这个问题中得到了答案:从内核分配用户空间内存......这是一个正确的解决方法吗?
  2. 如何从系统调用调用系统调用

编辑:

int sys_myfunc(const char __user * inputFileUser)   {


    char inputFile[255];
    int l = 0;
    while(inputFileUser[l] != '\0') l++;

    if(l==0)
        return -10; 

    if(copy_from_user(inputFile,inputFileUser,l+1)< 0 ) return -20;
//
//GENERATE fileName here
//
//

    char fileName[255];
    return  sys_renameat(AT_FDCWD, inputFile, AT_FDCWD, fileName);

}

以下仍然返回-1。为什么?我将数据复制到内核空间。

4

2 回答 2

5

我想准确地展示实现footy想要的正确方法,但是我的原始答案太长了,我决定将解决方案放在单独的答案中。我会将代码分成几部分,并解释每个片段的作用。

请记住,由于我们重用内核代码,因此本文中的代码和生成的函数必须在 GPLv2 许可下获得许可。

首先,我们首先声明一个单参数系统调用。

SYSCALL_DEFINE1(myfunc, const char __user *, oldname)
{

在内核中,堆栈空间是一种稀缺资源。您不创建本地数组;你总是使用动态内存管理。幸运的是,有一些非常有用的功能,例如__getname(),所以它是很少的附加代码。重要的是要记住在完成后释放您使用的任何内存。

由于这个系统调用基本上是 的变体rename,我们重用了几乎所有的fs/namei.c:sys_renameat()代码。首先,局部变量声明。也有很多;正如我所说,内核中的堆栈是稀缺的,在任何系统调用函数中你不会看到比这更多的局部变量:

    struct dentry *old_dir, *new_dir;
    struct dentry *old_dentry, *new_dentry;
    struct dentry *trap;
    struct nameidata oldnd, newnd;
    char *from;
    char *to = __getname();
    int error;

对 的第一个更改已经sys_renameat()char *to = __getname();上面的行中。它动态分配PATH_MAX+1字节,并且必须在__putname()不再需要后释放。这是为文件或目录名称声明临时缓冲区的正确方法。

要构造新路径 ( to),我们还需要能够直接访问旧名称 ( from)。由于内核-用户空间障碍,我们不能直接访问oldname。因此,我们创建它的内核副本:

    from = getname(oldname);
    if (IS_ERR(from)) {
        error = PTR_ERR(from);
        goto exit;
    }

尽管许多 C 程序员都被教导说goto是邪恶的,但这是个例外:错误处理。我们不必记住我们需要做的所有清理工作(__putname(to)至少我们已经需要做),我们将清理工作放在函数的末尾,然后跳到正确的点,exit也就是最后一个。error当然,保存错误号。

在我们函数的这一点上,我们最多可以访问from[0]first '\0',或者最多(包括)from[PATH_MAX],以先到者为准。它是一个普通的内核端数据,可以按照任何 C 代码中的普通方式访问。

您还为新名称保留了内存,to[0]包括to[PATH_MAX]. 请记住确保它也使用\0(在to[PATH_MAX] = '\0'或更早的索引中)终止。

在为 构建内容之后to,我们需要进行路径查找。不同renameat()的是,我们不能使用user_path_parent(). 然而,我们可以看看做什么user_path_parent(),然后做同样的工作——当然,适应我们自己的需要。事实证明它只是调用do_path_lookup()错误检查。因此,这两个user_path_parent()调用及其错误检查可以替换为

    error = do_path_lookup(AT_FDCWD, from, LOOKUP_PARENT, &oldnd);
    if (error)
        goto exit0;

    error = do_path_lookup(AT_FDCWD, to, LOOKUP_PARENT, &newnd);
    if (error)
        goto exit1;

请注意,这exit0是原始标签中找不到的新标签renameat()。我们需要一个新标签,因为在exit,我们只有to; 但是在exit0,我们同时拥有tofrom。在 之后exit0,我们有tofromoldnd,等等。

接下来,我们可以重用大部分sys_renameat(). 它完成了重命名的所有艰苦工作。为了节省空间,我将省略我对它的作用的漫谈,因为你可以相信,如果rename()有效,它也会有效。

    error = -EXDEV;
    if (oldnd.path.mnt != newnd.path.mnt)
        goto exit2;

    old_dir = oldnd.path.dentry;
    error = -EBUSY;
    if (oldnd.last_type != LAST_NORM)
        goto exit2;

    new_dir = newnd.path.dentry;
    if (newnd.last_type != LAST_NORM)
        goto exit2;

    error = mnt_want_write(oldnd.path.mnt);
    if (error)
        goto exit2;

    oldnd.flags &= ~LOOKUP_PARENT;
    newnd.flags &= ~LOOKUP_PARENT;
    newnd.flags |= LOOKUP_RENAME_TARGET;

    trap = lock_rename(new_dir, old_dir);

    old_dentry = lookup_hash(&oldnd);
    error = PTR_ERR(old_dentry);
    if (IS_ERR(old_dentry))
        goto exit3;
    /* source must exist */
    error = -ENOENT;
    if (!old_dentry->d_inode)
        goto exit4;
    /* unless the source is a directory trailing slashes give -ENOTDIR */
    if (!S_ISDIR(old_dentry->d_inode->i_mode)) {
        error = -ENOTDIR;
        if (oldnd.last.name[oldnd.last.len])
            goto exit4;
        if (newnd.last.name[newnd.last.len])
            goto exit4;
    }
    /* source should not be ancestor of target */
    error = -EINVAL;
    if (old_dentry == trap)
        goto exit4;
    new_dentry = lookup_hash(&newnd);
    error = PTR_ERR(new_dentry);
    if (IS_ERR(new_dentry))
        goto exit4;
    /* target should not be an ancestor of source */
    error = -ENOTEMPTY;
    if (new_dentry == trap)
        goto exit5;

    error = security_path_rename(&oldnd.path, old_dentry,
                     &newnd.path, new_dentry);
    if (error)
        goto exit5;

    error = vfs_rename(old_dir->d_inode, old_dentry,
                   new_dir->d_inode, new_dentry);

至此,所有的工作都完成了,只剩下释放上面代码占用的锁、内存等。如果此时一切都成功了error == 0,我们将进行所有清理工作。如果我们有问题,error包含错误代码,并且我们已经跳转到正确的标签以进行必要的清理,直到发生错误的地方。如果vfs_rename()失败——它执行实际操作——,我们将全部清理。

但是,与原始代码相比,我们from首先获得了 ( exit) ,紧随to( exit0) 之后,然后是 dentry 查找。因此,我们需要将它们释放到正确的位置(接近最后,因为它们是先完成的。当然,清理以相反的顺序进行):

exit5:
    dput(new_dentry);
exit4:
    dput(old_dentry);
exit3:
    unlock_rename(new_dir, old_dir);
    mnt_drop_write(oldnd.path.mnt);
exit2:
    path_put(&newnd.path);
exit1:
    path_put(&oldnd.path);
exit0:
    putname(from);
exit:
    __putname(to);
    return error;
}

到这里我们就完成了。

当然,上面我们复制的部分有很多细节需要考虑sys_renameat()——就像我在另一个答案中所说的那样,你不应该只是复制这样的代码,而是将通用代码重构为辅助函数;这使维护变得更加容易。幸运的是,因为我们保留了所有检查renameat()——我们在复制任何代码之前进行了路径操作renameat()——我们可以确保所有必要的检查都已完成。就好像用户自己指定了操纵路径并调用了renameat().

如果你在做一些检查之后再进行修改,情况会复杂得多。您必须考虑这些检查是什么,您的修改如何影响它们,并且几乎总是重新进行这些检查。

提醒任何读者,您不能只在自己的系统调用中创建文件名或任何其他字符串然后调用另一个系统调用的原因是您刚刚创建的字符串位于内核用户空间边界的内核一侧,而系统调用期望数据驻留在用户空间端。虽然在 x86 上,您可能会意外地从内核端刺穿边界,但这并不意味着您应该这样做:必须为此目的使用copy_from_user()andcopy_to_user()及其衍生物。调用另一个系统调用不是必须做魔术的问题,而是提供的数据在哪里(内核或用户空间)。strncpy_from_user()

于 2012-10-15T06:26:56.220 回答
2

嗯..linux-3.6.2/fs/namei.c包含许多类似的情况。例如,rename系统调用实际上定义为

SYSCALL_DEFINE2(rename, const char __user *, oldname, const char __user *, newname)
{
    return sys_renameat(AT_FDCWD, oldname, AT_FDCWD, newname);
}

换句话说,从另一个系统调用调用一个系统调用是没有问题的。问题是指针参数是用户空间指针,而您正在尝试提供内核指针:您fileName应该在用户空间中分配,但您的在内核空间中。

正确的解决方案是从两个函数(your 和sys_renameat()in fs/namei.c)中提取出公共代码,然后从两个系统调用中调用该函数。假设您没有尝试将其包含在上游——如果是,那么它是重构和重新思考的时间——您可以轻松地将内容复制sys_renameat到您自己的函数中;它不是那么大。熟悉诸如此类的文件系统操作所需的必要检查和锁定也是一个有用的点。


编辑以解释问题和解决方案:

在非常真实的意义上,普通进程分配的内存(用户空间内存)和内核分配的内存(内核空间)完全被内核-用户空间屏障隔开。

您的代码忽略了这个障碍,根本不应该工作。(它可能在 x86 上有点工作,因为在该体系结构上,内核用户空间屏障很容易从内核端穿透。)您还使用 256 字节的堆栈作为文件名,这是一个禁忌:内核堆栈是一个资源非常有限,应谨慎使用。

普通进程(用户空间进程)无法访问任何内核内存。你可以试试,不会的。这就是障碍存在的原因。(有些嵌入式系统的硬件根本不支持这种屏障,但为了讨论的目的,让我们忽略这些。记住,即使在 x86 上,屏障很容易从内核端突破,但这并不意味着它不存在。不要因为它似乎对你有用而假设,它在某种程度上是正确的。)

屏障的性质使得在大多数架构上,内核也存在屏障

为了帮助内核程序员,指向用户空间屏障的指针被标记为__user. 这意味着您不能仅仅取消引用它们并期望它们起作用;您需要使用copy_from_user()copy_to_user()。它不仅仅是系统调用参数:当您从内核访问用户空间数据时,您需要使用这两个函数。

所有系统调用都适用于用户空间数据。您看到的每个指针都已(或应该!)标记__user。每个系统调用都会完成所有必要的工作以从用户空间访问数据。

您的问题是您正在尝试向inputFile系统调用提供内核空间数据。它不会起作用,因为系统调用总是试图穿过屏障,但在屏障inputFile的同一侧!

真的没有理智的方法可以复制inputFile到障碍的另一边。我的意思是,当然有办法做到这一点,甚至没有那么困难,但它就是不理智的。

那么,让我们探索一下我上面描述的正确解决方案,以及哪个足球已经拒绝了一次。

首先,让我们看看renameat系统调用在当前 (3.6.2) Linux 内核中的实际样子(请记住,此代码是在 GPLv2 下获得许可的)。系统rename调用只是使用sys_renameat(AT_FDCWD, oldname, AT_FDCWD, newname). 我将插入我对代码作用的解释:

SYSCALL_DEFINE4(renameat, int, olddfd, const char __user *, oldname,
                int, newdfd, const char __user *, newname)
{
        struct dentry *old_dir, *new_dir;
        struct dentry *old_dentry, *new_dentry;
        struct dentry *trap;
        struct nameidata oldnd, newnd;
        char *from;
        char *to;
        int error;

在内核中,堆栈是一种有限的资源。您可以使用相当多的变量,但任何本地数组都会是一个严重的问题。上面的局部变量列表几乎是您在典型系统调用中看到的最大的。

对于重命名调用,该函数必须首先找到包含文件名的父目录:

        error = user_path_parent(olddfd, oldname, &oldnd, &from);
        if (error)
                goto exit;

注意:在此之后,旧的目录和路径必须在使用后通过调用释放path_put(&oldnd.path); putname(from);

        error = user_path_parent(newdfd, newname, &newnd, &to);
        if (error)
                goto exit1;

注意:在此之后,新的目录和路径必须在使用后通过调用释放path_put(&newnd.path); putname(to);

下一步是检查两者是否位于同一文件系统上:

        error = -EXDEV;
        if (oldnd.path.mnt != newnd.path.mnt)
                goto exit2;

目录中的最后一个组件必须是普通目录:

        old_dir = oldnd.path.dentry;
        error = -EBUSY;
        if (oldnd.last_type != LAST_NORM)
                goto exit2;

        new_dir = newnd.path.dentry;
        if (newnd.last_type != LAST_NORM)
                goto exit2;

并且包含目录的挂载必须是可写的。请注意,如果成功,这将对挂载应用锁定,并且必须始终mnt_drop_write(oldnd.path.mnt)在系统调用返回之前与调用配对。

        error = mnt_want_write(oldnd.path.mnt);
        if (error)
                goto exit2;

接下来,更新 nameidata 查找标志以反映目录是已知的:

        oldnd.flags &= ~LOOKUP_PARENT;
        newnd.flags &= ~LOOKUP_PARENT;
        newnd.flags |= LOOKUP_RENAME_TARGET;

接下来,这两个目录在重命名期间被锁定。这必须与相应的解锁调用配对,unlock_rename(new_dir, old_dir)

        trap = lock_rename(new_dir, old_dir);

接下来,查找实际存在的文件。如果成功,则必须通过调用释放 dentry dput(old_dentry)

        old_dentry = lookup_hash(&oldnd);
        error = PTR_ERR(old_dentry);
        if (IS_ERR(old_dentry))
                goto exit3;
        /* source must exist */
        error = -ENOENT;
        if (!old_dentry->d_inode)
                goto exit4;
        /* unless the source is a directory trailing slashes give -ENOTDIR */
        if (!S_ISDIR(old_dentry->d_inode->i_mode)) {
                error = -ENOTDIR;
                if (oldnd.last.name[oldnd.last.len])
                        goto exit4;
                if (newnd.last.name[newnd.last.len])
                        goto exit4;
        }
        /* source should not be ancestor of target */
        error = -EINVAL;
        if (old_dentry == trap)
                goto exit4;

还会查找新文件名的条目(毕竟它可能存在)。同样,如果成功,这个 dentry 也必须在dput(new_dentry)之后使用:

        new_dentry = lookup_hash(&newnd);
        error = PTR_ERR(new_dentry);
        if (IS_ERR(new_dentry))
                goto exit4;
        /* target should not be an ancestor of source */
        error = -ENOTEMPTY;
        if (new_dentry == trap)
                goto exit5;

此时,该功能已确定一切正常。接下来,它必须通过调用来检查操作是否可以继续(关于访问模式等)security_path_rename(struct path *old_dir, struct dentry *old_dentry, struct path *new_dir, struct dentry *new_dentry)。(用户空间进程的身份详细信息保存在 中current。)

        error = security_path_rename(&oldnd.path, old_dentry,
                                     &newnd.path, new_dentry);
        if (error)
                goto exit5;

如果对重命名没有异议,则可以使用以下方法进行实际重命名vfs_rename(struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry)

        error = vfs_rename(old_dir->d_inode, old_dentry,
                           new_dir->d_inode, new_dentry);

至此,所有的工作都完成了(如果error为零则成功),剩下的就是释放各种lookup

exit5:
        dput(new_dentry);
exit4:
        dput(old_dentry);
exit3:
        unlock_rename(new_dir, old_dir);
        mnt_drop_write(oldnd.path.mnt);
exit2:
        path_put(&newnd.path);
        putname(to);
exit1:
        path_put(&oldnd.path);
        putname(from);
exit:
        return error;
}

这就是重命名操作。如您所见,没有明确copy_from_user()的可见。user_path_parent()调用它的getname()调用getname_flags(),它执行它。如果您忽略所有必要的检查,则归结为

char *result = __getname();  /* Reserve PATH_MAX+1 bytes of kernel memory for one file name */
in    len;

len = strncpy_from_user(result, old/newname, PATH_MAX);
if (len <= 0) {
    __putname(result);
    /* An error occurred, abort! */
}

if (len >= PATH_MAX) {
    __putname(result);
    /* path is too long, abort! */
}

/* Finally, add it to the audit context for the current process. */
audit_getname(result);

并且,在不再需要它之后,

putname(result);

所以,footy,你的问题没有简单的解决方案。没有一个函数调用可以神奇地使您的系统调用工作。您将不得不重写它,看看这些东西是如何在fs/namei.c. 这并不难,但是你必须小心谨慎地去做——而且最重要的是接受“只是试图让这个简单的事情以最小的改变工作”的方法不适用于此。

于 2012-10-14T00:40:06.910 回答