Linux: 0, 1, 2 のファイルディスクリプタを閉じて setuid したバイナリ実行の挙動を調べる

以下の記事を読んで setuid したバイナリを実行する挙動で新たに知ったことがあった

lwn.net

以下に引用する

Some OSes (e.g., OpenBSD) protect against this by opening /dev/null on any unused FDs in the 0-2 range when execing a setuid program. As far as I can tell, Linux does not (but maybe I'm missing something...). This behavior is permitted in POSIX.1-2001, but not before.


いくつかの OS (たとえば OpenBSD) は、setuid プログラムを実行するときに、0-2 の範囲の未使用の FD で /dev/null をオープンして、この問題を防いでいます。私の知る限り、Linuxはそうではありません(しかし、もしかしたら私は何かを見逃しているかもしれません...)。この動作はPOSIX.1-2001では許可されているが、それ以前は許可されていない。

DeepL 翻訳

OpenBSD の execve(2) の man にも 下記の説明が付いている

     In the case of a new setuid or setgid executable being exe-
     cuted, if file descriptors 0, 1, or 2 (representing stdin, stdout, and
     stderr) are currently unallocated, these descriptors will be opened to
     point to some system file like /dev/null. The intent is to ensure these
     descriptors are not unallocated, since many libraries make assumptions
     about the use of these 3 file descriptors.

なるほどなー 。実際にどうなんだろうと Linux で試した。

実験環境

Ubuntu Jammy で実験をします

hiboma@vps:~$ uname -a
Linux vps 5.15.0-60-generic #66-Ubuntu SMP Fri Jan 20 14:29:49 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

hiboma@vps:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04 LTS
Release:    22.04
Codename:   jammy

実験用のコード

C のコードで sleep するだけの setuid バイナリを用意します。

#include <unistd.h>

int main() {
    sleep(100);
}

以下の手順で setuid なバイナリとします。

$ gcc setuid-sleep.c -o setuid-sleep
$ sudo chown root.root setuid-sleep 
$ sudo chmod 4755 setuid-sleep 

$ ls -hal setuid-sleep
-rwsr-xr-x 1 root root 16K Jun  7 10:24 setuid-sleep

0, 1, 2 のファイルディスクリプタを閉じてから 1 setuid なバイナリを実行する bashシェルスクリプトも用意します

#!/bin/bash                                                                                                                                                                                                      
                                                                                                                                                                                                                 
exec 0<&-                                                                                                                                                                                                        
exec 1<&-                                                                                                                                                                                                        
exec 2<&-                                                                                                                                                                                                        
                                                                                                                                                                                                                 
exec ./setuid-sleep                                                                                                                                                                                              

実験

シェルスクリプトを実行します。これで 0, 1, 2 のデスクリプタを閉じて setuid したバイナリを exec できます。

$ ./test.sh

setuid したバイナリを実行しているプロセスの lsof をとって見ます。

$ sudo lsof -p $(pgrep setuid-sleep)
COMMAND       PID USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
setuid-sl 1120951 root  cwd    DIR  252,2     4096 134434 /home/hiboma
setuid-sl 1120951 root  rtd    DIR  252,2     4096      2 /
setuid-sl 1120951 root  txt    REG  252,2    15968 135566 /home/hiboma/setuid-sleep
setuid-sl 1120951 root  mem    REG  252,2  2216304   4337 /usr/lib/x86_64-linux-gnu/libc.so.6
setuid-sl 1120951 root  mem    REG  252,2   240936    194 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
setuid-sl 1120951 root    0w   CHR    1,7      0t0      8 /dev/full 👈
setuid-sl 1120951 root    1r   CHR    1,3      0t0      5 /dev/null 👈
setuid-sl 1120951 root    2r   CHR    1,3      0t0      5 /dev/null 👈

/dev/full, /dev/null を開いていますね!

/dev/full, /dev/null を open するのはどこ?

strace をとって調べて見たところ、ld-linux-x86-64.so.2 っぽい。

openat(AT_FDCWD, "/dev/full", O_WRONLY|O_NOFOLLOW) = 0
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x9748) [0x26b38]
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x903e) [0x2642e]
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x24a5) [0x1f895]
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x41c8) [0x215b8]
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x2ec8) [0x202b8]

... 略


openat(AT_FDCWD, "/dev/null", O_RDONLY|O_NOFOLLOW) = 1
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x9748) [0x26b38]
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x907e) [0x2646e]
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x24a5) [0x1f895]
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x41c8) [0x215b8]
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x2ec8) [0x202b8]

... 略


openat(AT_FDCWD, "/dev/null", O_RDONLY|O_NOFOLLOW) = 2
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x9748) [0x26b38]
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x90c2) [0x264b2]
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x24a5) [0x1f895]
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x41c8) [0x215b8]
 > /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2(_dl_catch_error+0x2ec8) [0x202b8]

ソースは?

  • どういう実装になっているのかも気になって glibc のソースを辿ってみたのでしたが、分からず
  • _dl_catch_error 周りも複雑でわからん。 slack-gpt の回答は以下の通りだった

_dl_catch_errorは、GNU Cライブラリ (glibc) の内部で使用される関数です。この関数は、動的リンカーによる共有ライブラリのロードやシンボルの解決中に発生するエラーをキャッチ(捉え)する役割を果たします。


具体的には、_dl_catch_errorは以下のような機能を提供します: 1. エラーハンドラの設定: エラーが発生した際に呼び出される関数を設定できます。これにより、バリエーションに富むエラーハンドリングを行うことができます。

  1. エラーの発生処理: _dl_catch_error は、エラーが発生した際にエラーメッセージを生成し、設定されたエラーハンドラに渡します。これにより、詳細なエラー情報が収集され、適切なエラー処理が行われます。

  2. エラーハンドラのリセット: エラーが捉えられた後には、元の状態に戻します。_dl_catch_errorはビルトインのエラー処理機能を提供することで、動的リンカーによる共有ライブラリのロードやシンボルの解決を安全かつ信頼性の高いものにする役割を果たします

実験の補足

ファイルディスクリプタを閉じずに setuid バイナリを実行すると、0, 1, 2 のファイルディスクリプタ/dev/pts を指していました

~$ sudo lsof -p $(pgrep setuid-sleep)
COMMAND       PID USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
setuid-sl 1125289 root  cwd    DIR  252,2     4096 134434 /home/hiboma
setuid-sl 1125289 root  rtd    DIR  252,2     4096      2 /
setuid-sl 1125289 root  txt    REG  252,2    15968 135566 /home/hiboma/setuid-sleep
setuid-sl 1125289 root  mem    REG  252,2  2216304   4337 /usr/lib/x86_64-linux-gnu/libc.so.6
setuid-sl 1125289 root  mem    REG  252,2   240936    194 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
setuid-sl 1125289 root    0u   CHR  136,1      0t0      4 /dev/pts/1
setuid-sl 1125289 root    1u   CHR  136,1      0t0      4 /dev/pts/1
setuid-sl 1125289 root    2u   CHR  136,1      0t0      4 /dev/pts/1

感想

  • 細かいプロセスの挙動ではまだまだ知らないことがある
  • /dev/full, /dev/null を open する実装がどこにあるのか気になる

関連エントリ

hiboma.hatenadiary.jp

hiboma.hatenadiary.jp


  1. シェルスクリプトでファイルディスクリプタを閉じる、って全然書いたことがなくて やり方を知らなかった