如何在Linux 3.5.4中从自定义系统调用中调用系统调用

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的错误。我从这个问题得到了答案:allocate user-space memory from kernel…这种方法可行吗?
  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。为什么?我已经将数据复制到内核空间了。
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 是邪恶的,但这里是个例外: 错误处理。我们将清理工作放在函数末尾,跳转到正确的点,exit 是最后一个。这样就不用记住我们需要做的所有清理工作了(我们至少需要做 __putname(to))。当然,error 保存错误号。

在函数的这一点,我们可以访问 from[0] 到第一个 '\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时,我们既有to又有from。在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之后),接着是目录项查找。因此,我们需要将它们的释放移到它们正确的位置上(靠近最后,因为它们是首先完成的。当然,清理是按照相反的顺序进行的):

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()copy_to_user() 及其衍生物,如 strncpy_from_user() 必须用于此目的。这不是一个关于必须进行魔法来调用另一个系统调用的问题,而是关于提供的数据位于何处(内核空间还是用户空间)。


感谢您提供如此精彩的答案。我会在今晚实现它(并在这里更新)。这个答案值得赞赏。再次感谢。也许是因为互联网上没有太多易于参考的资料,所以我之前不知道这个。我认为这个问题应该有一个更好的标题,以便像我这样被卡住的人可以更容易地进行SEO。 - footy
如果您有空的话,我有另一个问题希望您能看一下:http://stackoverflow.com/questions/12886593/how-to-get-the-files-meta-data-in-kernel-space-linux-3-5 ... 如果您有时间,请查看一下。我不知道为什么会被踩。 - footy
我特意为你开了一个悬赏并给了它。这个答案值得你的努力!谢谢。 - footy

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 应该在用户空间分配,但实际上在内核空间分配了。
正确的解决方案是将两个函数(你自己的函数和 fs/namei.c 中的 sys_renameat())中的公共代码分离出来,然后从两个系统调用中调用该函数。假设你不想让此代码上游包含 - 如果是这样,那么现在是重构和重新思考的时候了 - 你可以轻松地将 sys_renameat 的内容复制到你自己的函数中;它并不大。这也是熟悉文件系统操作所需的必要检查和锁定的有用点。
为了解释问题和解决方案而进行编辑:
从非常真实的意义上讲,由正常进程(用户空间内存)分配的内存和由内核(内核空间)分配的内存完全被内核-用户空间屏障分隔开来。
你的代码忽略了这个屏障,根本不应该工作。(它可能在 x86 上部分工作,因为在该体系结构上从内核端很容易穿过内核-用户空间屏障。)你还使用了 256 字节的栈来存储文件名,这是不对的:内核栈是一种非常有限的资源,应该节约使用。
正常进程(用户空间进程)无法访问任何内核内存。你可以试试,但它行不通。这就是屏障存在的原因。(有些嵌入式系统具有硬件,根本不支持这样的屏障,但为了本次讨论,我们将忽略这些。请记住,即使在 x86 上,屏障很容易从内核端穿过,也并不意味着它不存在。不要表现得像个混蛋,认为因为它对你似乎有效,所以它是正确的。)
屏障的性质是,在大多数架构上,内核也存在屏障。
为了帮助内核程序员,指向用户空间的指针被标记为 __user。这意味着你不能只引用它们并期望它们工作;你需要使用 copy_from_user() 和 copy_to_user()。这不仅适用于系统调用参数:当你从内核访问用户空间数据时,你需要使用这两个函数。
所有系统调用都在用户空间数据上工作。你看到的每个指针都应该被标记为 __user。每个系统调用都会执行访问用户空间数据所需的所有工作。
你的问题在于,你试图向系统调用提供内核空间数据 inputFile。它不会工作,因为系统调用总是尝试穿过屏障,但 inputFile 在屏障的同一侧!

没有什么明智的方法可以将 inputFile 复制到屏障的另一侧。当然,有一些方法可以实现,而且也不是很难,但这并不明智。

因此,让我们探讨上述正确解决方案,这个方案已经被 footy 拒绝了一次。

首先,让我们看看当前(3.6.2)Linux内核中 renameat 系统调用的实际情况(请记住,该代码受 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);

接下来,将查找实际存在的文件。如果成功,需要通过调用dput(old_dentry)来释放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;

新文件名的目录项也被查找(毕竟它可能已经存在)。如果成功,同样需要使用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为零,则成功),唯一剩下的就是释放各种查找。

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中正确处理的内容。这并不难,但你必须小心谨慎地去做,并且最重要的是接受这样一种方法:“只试图用最少的更改让这个简单的东西工作”对此无效。


抱歉,你能重新表达第二段吗?我是个新手。 - footy
2
@footy: 内核和普通进程之间有一个非常明显的边界。你试图忽略这个边界。在内核中,你必须使用copy_from_user()从用户空间获取数据,并使用copy_to_user()将数据存储到用户空间以通过边界。这就是为什么指针被标记为__user:提醒你。内核代码调用其他内核代码没有问题。你的问题在于你在内核侧具有fileName,而你正在调用的函数期望它在另一侧,即用户空间的一侧。 - Nominal Animal
那么我所要做的就是使用copy_from_user()函数复制我的数据?这样就可以消除这个hack了 :) PS:感谢您提供的精彩解释! - footy
1
@footy:你的代码没有修复的方法,它全部都是错的。你需要完全重写。我在这个答案中已经详细解释了为什么以及如何重写,并在一个新的答案中提供了一个示例。 - Nominal Animal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接