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;

}

ここで2つの疑問があります。

  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

私は、フッティが望んでいることを達成するための正しい方法を正確に示したかったのですが、元の答えが長すぎたため、別の答えに解決策を入れることにしました。コードをいくつかの部分に分割し、各フラグメントが何をするかを説明します。

カーネルコードを再利用するため、この投稿のコードと結果の関数はGPLv2ライセンスでライセンスされている必要があることに注意してください。

まず、1パラメータのシステムコールを宣言することから始めます。

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

カーネルでは、スタックスペースは不足しているリソースです。ローカル配列は作成しません。常に動的メモリ管理を使用します。幸い、のような非常に便利な関数がいくつかある__getname()ので、追加のコードはほとんどありません。重要なことは、使い終わったら、使用しているメモリをすべて解放することを忘れないことです。

このシステムコールは基本的にのバリアントであるrenameため、ほとんどすべてのfs/namei.c:sys_renameat()コードを再利用します。まず、ローカル変数の宣言。たくさんあります。私が言ったように、カーネルではスタックが不足しており、どのsyscall関数でもこれよりもはるかに多くのローカル変数が表示されることはありません。

    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;
    }

goto多くのCプログラマーは悪であると教えられてきましたが、これは例外です:エラー処理。実行する必要のあるすべてのクリーンアップを覚えておく必要はありません(そして__putname(to)、少なくとも実行する必要があります)。関数の最後にクリーンアップを配置し、最後のポイントである正しいポイントにスキップしますexiterrorもちろん、エラー番号を保持します。

関数のこの時点でfrom[0]、最初の'\0'、または最大(およびそれを含む)from[PATH_MAX]のいずれか早い方にアクセスできます。これは通常のカーネル側のデータであり、Cコードで行う通常の方法でアクセスされます。

to[0]また、までの新しい名前のメモリを予約しましたto[PATH_MAX]\0(into[PATH_MAX] = '\0'または以前のインデックス)を使用して終了することも忘れないでください。

のコンテンツを作成した後to、パスルックアップを実行する必要があります。とは異なりrenameat()、は使用できませんuser_path_parent()。しかし、私たちは何をするのかを見user_path_parent()て、同じ仕事をすることができます-もちろん、私たち自身のニーズに適応します。do_path_lookup()エラーチェックで呼び出すだけであることがわかりました。したがって、2つの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に、、、、などがtoありfromますoldnd

次に、の大部分を再利用でき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()です。

いくつかのチェックがすでに行われた後に変更を行う場合、状況ははるかに複雑になります。それらのチェックが何であるか、変更がそれらにどのように影響するかを考え、ほとんどの場合、それらのチェックをやり直す必要があります。

読者に思い出させるために、自分のsyscallでファイル名やその他の文字列を作成してから別のsyscallを呼び出すことができない理由は、作成したばかりの文字列がカーネルとユーザースペースの境界のカーネル側にあるのに対し、syscallはもう一方のユーザースペース側に存在するデータ。x86では、カーネル側から誤って境界を突き破ることができますが、そうする必要があるという意味ではありません。この目的にはcopy_from_user()copy_to_user()とその派生物strncpy_from_user()を使用する必要があります。別のシステムコールを呼び出すために魔法をかけなければならないという問題ではなく、提供されたデータがどこにあるか(カーネル内またはユーザースペース)についてです。

于 2012-10-15T06:26:56.220 に答える
2

うーん..linux-3.6.2/fs/namei.c似たようなシチュエーションがたくさんあります。たとえば、renamesyscall は実際には次のように定義されます。

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

つまり、システムコールから別のシステムコールを呼び出しても問題ありません。問題は、ポインター引数がユーザー空間ポインターであるのに対し、カーネルポインターを提供しようとしているということです。fileNameユーザー空間に割り当てる必要がありますが、カーネル空間にある必要があります。

正しい解決策は、2 つの関数 ( yours とsys_renameat()in fs/namei.c) から共通のコードを抜き出し、両方のシステムコールから関数を呼び出すことです。これをアップストリームに含めようとしていないと仮定すると-もしそうなら、それはリファクタリングと再考の時間です-の内容をsys_renameat自分の関数に簡単にコピーできます。それほど大きくありません。また、このようなファイルシステム操作に必要なチェックとロックについて理解することも役立ちます。


問題と解決策を説明するために編集されました。

非常に現実的な意味では、通常のプロセスによって割り当てられたメモリ (ユーザー空間メモリ) とカーネルによって割り当てられたメモリ (カーネル空間) は、カーネルとユーザー空間のバリアによって完全に分離されています。

あなたのコードはその障壁を無視しているため、まったく機能しません。(x86 では、カーネルとユーザー空間の障壁がカーネル側から簡単に突き破られるため、おそらく多少は機能します。) また、ファイル名に 256 バイトのスタックを使用しますが、これは禁物です: カーネル スタックはリソースは非常に限られているため、慎重に使用する必要があります。

通常のプロセス (ユーザー空間プロセス) は、カーネル メモリにアクセスできません。あなたは試すことができます、それはうまくいきません。これが障壁が存在する理由です。(そのようなバリアをサポートしていないハードウェアを備えた特定の組み込みシステムがありますが、この議論の目的のためにそれらを無視しましょう。x86 ではバリアはカーネル側から簡単に突き破ることができますが、それが意味するものではないことを覚えておいてください。そこにはありません.あなたのために働くように見えるので、それはどういうわけか正しいと思い込まないでください.)

バリアの性質として、ほとんどのアーキテクチャではカーネルにもバリアが存在します

カーネル プログラマーを支援するために、バリアを越えてユーザー空間を指すポインターは とマークされてい__userます。これは、それらを逆参照して動作することを期待することはできないことを意味します。と を使用する必要がありcopy_from_user()ますcopy_to_user()。syscall パラメーターだけではありません。カーネルからユーザー空間データにアクセスするときは、これら 2 つの関数を使用する必要があります。

すべての syscall は、ユーザー空間データに対して機能します。表示されるすべてのポインターには、 マークが付いています (またはマークする必要があります) __user。すべての syscall は、ユーザー空間からデータにアクセスするために必要なすべての作業を行います。

問題は、カーネル空間データ をシステムコールに提供しようとしていることですinputFileinputFileシステムコールは常にバリアを通過しようとしますが、バリアの同じ側にあるため、機能しません!

inputFileバリアの反対側にコピーする正気の方法は実際にはありません。もちろん、それを行う方法はありますし、それほど難しくはありませんが、正気ではありません。

それでは、私が上で説明した正しい解決策を探ってみましょう。

まず最初に、renameatsyscall が現在の (3.6.2) Linux カーネルで実際にどのように見えるかを見てみましょう (このコードは GPLv2 の下でライセンスされていることに注意してください)。syscallは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;

カーネルでは、スタックは限られたリソースです。かなりの数の変数を使用できますが、ローカル配列は深刻な問題になります。上記のローカル変数リストは、典型的なシステムコールで見られる最大のものです。

rename 呼び出しの場合、関数は最初にファイル名を含む親ディレクトリを見つける必要があります。

        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);

次のステップは、2 つが同じファイルシステムに存在することを確認することです。

        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;

次に、名前変更の間、2 つのディレクトリがロックされます。これは、対応するロック解除呼び出しと組み合わせる必要があります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)。(ユーザー空間プロセスの ID の詳細は で維持され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);

だから、あなたの問題に対する簡単な解決策はありません。システムコールを魔法のように機能させる単一の関数呼び出しはありません。でどのように適切に処理されているかを見て、書き直す必要がありますfs/namei.c。難しいことではありませんが、慎重かつ細心の注意を払って行う必要があります。そして何よりも、「この単純なことを最小限の変更で機能させようとする」というアプローチではうまくいかないことを受け入れます。

于 2012-10-14T00:40:06.910 に答える