コンテナ/サンドボックスを設定する一般的なシナリオは、新しいtmpfsに最小限のデバイスノードセットを作成することです(ホストを公開するのとは対照的に/dev
)。私が知っている唯一の(権限がない)方法はバインドマウントすることです。あなたが望むもの。私が(内部的にunshare -mc --keep-caps
)使用するコマンドは次のとおりです。
mkdir /tmp/x
mount -t tmpfs none /tmp/x
touch /tmp/x/null
mount -o bind /dev/null /tmp/x/null
マウントを上部に移動する予定です/dev
。ただし、移動を実行する前に実行すると、echo > /tmp/x/null
「許可拒否」エラー(EACCES
)が発生します。
しかし、別の方法で実行すると、次のようになります。
mkdir /tmp/x/y
touch /tmp/x/y/null
mount -o bind /dev/null /tmp/x/y/null
echo > /tmp/x/y/null
期待どおりに書き込みが成功します。私はこれをかなりたくさん試しましたが、根本的な原因、なぜこれが起こるのかを見つけることができません。この問題は、バインドマウントされたノードをサブディレクトリに配置し、そのシンボリックリンクを新しいファイルシステムになる最上位レベルに配置することで解決できますが、必ずしも/dev
必要ではないようです。
どうなりますか?これについて合理的な説明はありますか?それとも、一部のアクセス制御ロジックに問題がありますか?
答え1
まあ、これは3つのメカニズムが一緒に結合された結果である非常に興味深い効果のようです。
最初の(マイナーな)ポイントは、何かをファイルにリダイレクトするときにシェルがファイルがO_CREAT
まだ存在しない場合に作成されることを保証するオプションを使用してターゲットファイルを開くことです。
2番目に考慮すべき点はマウントポイントですが、通常の/tmp/x
ディレクトリです。オプションなしでマウントすると仮定すると、マウントポイントに対する権限が自動的に変更され、グローバルに書き込み可能になり、固定ビット(これは一般的な権限セットであり、合理的なデフォルト値のように感じる)を持ちます。可能です(あなたの状況によって異なります)。tmpfs
/tmp/x/y
tmpfs
1777
/tmp
/tmp/x/y
0755
umask
最後に、3番目のパズルのピースは、ユーザーの名前空間が設定される方法です。つまり、unshare(1)
ホストユーザーのUID / GIDを新しい名前空間の同じUID / GIDにマップするように指示します。これはただ新しい名前空間にマッピングされるため、親/子名前空間間で異なるUIDを変換しようとすると、呼び出される結果が発生します。オーバーフローUID、デフォルトは65534
-userですnobody
(user_namespaces(7)
セクションを参照Unmapped user and group IDs
)。これは/dev/null
、子ユーザーの名前空間が内部的に所有するバインドマウントを作成します(子ユーザーの名前空間にはnobody
ホストユーザーへのマッピングがないため)。root
$ ls -l /dev/null
crw-rw-rw- 1 nobody nobody 1, 3 Nov 25 21:54 /dev/null
すべての事実をまとめると、次のような結論に達します。ファイルがグローバルに書き込み可能な固定ディレクトリにあり、ファイルを含むディレクトリの所有者以外のユーザーが所有している間、オプションを使用して既存のecho > /tmp/x/null
ファイルを開こうとしました。O_CREAT
nobody
今、openat(2)
一言ずつ注意深く読んでみてください。
ヨーロッパ航空宇宙センター
O_CREAT が指定され、protected_fifos または protected_regular sysctl が有効になり、ファイルが存在し、FIFO または一般ファイルであり、ファイル所有者が現在のユーザーでも含まれていないディレクトリの所有者でもなく、インクルードディレクトリはすべてワールドファイルまたは一般ファイルです。グループ書き込み可能および粘度。詳細については、proc(5) の /proc/sys/fs/protected_fifos および /proc/sys/fs/protected_regular の説明を参照してください。
本当にいいじゃないですか?これは私たちの状況にほぼ似ています...ただ一般ファイルとFIFOについて教えてください。何もないデバイスノード情報。
わかりました。実際にこれを実装するコード。基本的に、最初に成功する必要がある例外条件(最初の条件)を確認し、固定ディレクトリがグローバルに書き込み可能な場合は、他の条件(2番目、1番目の条件)へのif
アクセスを拒否することがわかります。if
static int may_create_in_sticky(umode_t dir_mode, kuid_t dir_uid,
struct inode * const inode)
{
if ((!sysctl_protected_fifos && S_ISFIFO(inode->i_mode)) ||
(!sysctl_protected_regular && S_ISREG(inode->i_mode)) ||
likely(!(dir_mode & S_ISVTX)) ||
uid_eq(inode->i_uid, dir_uid) ||
uid_eq(current_fsuid(), inode->i_uid))
return 0;
if (likely(dir_mode & 0002) ||
(dir_mode & 0020 &&
((sysctl_protected_fifos >= 2 && S_ISFIFO(inode->i_mode)) ||
(sysctl_protected_regular >= 2 && S_ISREG(inode->i_mode))))) {
const char *operation = S_ISFIFO(inode->i_mode) ?
"sticky_create_fifo" :
"sticky_create_regular";
audit_log_path_denied(AUDIT_ANOM_CREAT, operation);
return -EACCES;
}
return 0;
}
したがって、ターゲットファイルがキャラクタデバイス(通常のファイルまたはFIFOではない)の場合、O_CREAT
カーネルはグローバルに書き込み可能な固定ディレクトリにあるときにファイルを開くことを拒否します。
原因が正しく検出されたことを証明するために、次のいずれかの条件で問題が消えていることを確認できます。
- インストール - これ
tmpfs
は-o mode=777
いいえマウントポイントに固定ビットがあるようにします。 /tmp/x/null
で開くがO_WRONLY
オプションはありません(これをテストするには、O_CREAT
呼び出すプログラムを作成、コンパイル、実行して各呼び出しの戻り値を確認してください)。open("/tmp/x/null", O_WRONLY | O_CREAT)
open("/tmp/x/null", O_WRONLY)
strace -e trace=openat
この動作をカーネルのバグと見なす必要があるかどうかはわかりませんが、ドキュメントではこれについてopenat(2)
説明しないようです。みんなシステムが呼び出される状況実際に失敗して表示されますEACCES
。