procfs で遊ぶ

こんにちは、masm11 です。

Linux の procfs をご存知でしょうか。 疑似ファイルシステムの一つで、通常は /proc/ に mount されていて、 その中でも /proc/PID はプロセスに関するいろいろな情報を見せて くれます。

今回は、/proc/PID を使って何ができるかを見てみたいと思います。

なお、/proc/PIDPID の箇所にはプロセスIDを指定します。 ただし、/proc/self で、そのプロセスのプロセスIDを指定したことにする 指定方法もあります。

環境変数

いきなりですが、

vim /proc/self/environ

と実行してみましょう。

GDK_DPI_SCALE=1^@XDG_ACTIVATION_TOKEN=kwin-7^@TERMINATOR_UUID=urn:uuid:e4ba8e93-e42f-4e16-8e01-00dec
c17e9ca^@KDE_SESSION_UID=1000^@DESKTOP_SESSION=plasmawayland^@PWD=/home/masm^@CLICOLOR=1^@WAYLAND_DI
SPLAY=wayland-0^@VISUAL=vi^@GDK_SCALE=1^@AURDEST=/aur^@LOGNAME=masm^@ANDROID_EMULATOR_USE_SYSTEM_LIB
S=1^@MOZ_ENABLE_WAYLAND=1^@MAIL=/var/spool/mail/masm^@EDITOR=vi^@XDG_SESSION_TYPE=wayland^@SYSTEMD_E

実行すると、vim が色を付けてくれるので、もう少し見やすいと思います。

環境変数が見えました。 複数の環境変数が ^@ で区切られて表示されています。

「どうにもうまく動かんなぁ... 環境変数ちゃんと渡ってないのかしら...」 と疑問に思ったら、これで確認できます。

「環境変数なら外から見えないので安全」とか思ってるそこのあなた、 やばいかもしれませんね。

メモリマップ

あまりないかもしれませんが、 shared library (*.so) がちゃんと読めてるのか疑問に思うことがあります。

ldconfig -p である程度表示できますが、 「で、結局どうなん??」と思うこともあります。

直接メモリマップを見てしまいましょう。

vim /proc/self/maps

↑これを実行すると、↓こんな感じに表示されます。

55c6abfad000-55c6ac032000 r--p 00000000 00:19 204122                     /usr/bin/vim
55c6ac032000-55c6ac346000 r-xp 00085000 00:19 204122                     /usr/bin/vim
55c6ac346000-55c6ac3d1000 r--p 00399000 00:19 204122                     /usr/bin/vim
55c6ac3d1000-55c6ac3e4000 r--p 00424000 00:19 204122                     /usr/bin/vim
55c6ac3e4000-55c6ac411000 rw-p 00437000 00:19 204122                     /usr/bin/vim
55c6ac411000-55c6ac41f000 rw-p 00000000 00:00 0
55c6adb4b000-55c6adc10000 rw-p 00000000 00:00 0                          [heap]
7f4ef806b000-7f4ef80b5000 r--p 00000000 00:19 344829                     /usr/share/vim/vim90/lang/ja/LC_MESSAGES/vim.mo
7f4ef80b5000-7f4ef85eb000 r--p 00000000 00:19 819739                     /usr/lib/locale/locale-archive
7f4ef85eb000-7f4ef85ee000 rw-p 00000000 00:00 0
7f4ef85ee000-7f4ef85f0000 r--p 00000000 00:19 13542                      /usr/lib/libcrypt.so.2.0.0

/usr/bin/vim がどこにロードされてて、/usr/lib/libcrypt.so.2.0.0 がどこにロードされてて、 というのがすべて表示されます。

もし万が一、ディスク上の別の場所にある同じ名前の違うバージョンの shared library が 両方読み込まれていたら、まずいですね。違うバージョンとはいえ、同じシンボルはあるでしょうから、 どちらが呼ばれるかわかりません。

カレントディレクトリ

/proc/PID を使うと、そんなものまでわかります。

luna:~ % ls -l /proc/self/cwd
lrwxrwxrwx 1 masm masm 0  926 19:30 /proc/self/cwd -> /home/masm/

カレントディレクトリを指しています。

あまり用途はないですが、たまに役に立つこともあります。

Rails アプリを /data/xxx/releases/YYYYMMDD_HHMMSS/ にデプロイしてあるルールとします。 「なんかおかしいなぁ... 古いプロセスが混ざってるんじゃ…?」と疑問を感じたら、 実行中の Unicorn の PID をすべて列挙し、それらの /proc/PID/cwd を確認すれば、 古いプロセスかどうかわかります。

また、chroot の檻の中で /proc に procfs を mount してある場合、 PID に檻の外にいるプロセスの PID を指定すると、 /proc/PID/cwd から外に出られてしまいます。 「chroot をセキュリティ用途に使うな」というのは、この辺からも解りますね。

「え? /proc/PID/cwd は symbolic link なんだから、外には出られないでしょ?」 と思われた方。するどいです。 実は symbolic link は ls が見せてる仮の姿で、本当は実体なんです。 以下に実証します。

luna:t % mkdir root
# procfs を mount
luna:t % mkdir root/proc
luna:t % sudo mount --bind /proc root/proc
# bash を用意
luna:t % mkdir root/bin
luna:t % mkdir root/lib64
luna:t % mkdir -p root/usr/lib
luna:t % cp /usr/lib/libreadline.so.8 root/usr/lib/
luna:t % cp /usr/lib/libdl.so.2 root/usr/lib/
luna:t % cp /usr/lib/libc.so.6 root/usr/lib/
luna:t % cp /usr/lib/libncursesw.so.6 root/usr/lib/
luna:t % cp /lib64/ld-linux-x86-64.so.2 root/lib64 
luna:t % cp /bin/bash root/bin/
# chroot する
luna:t % sudo chroot root /bin/bash
# procfs 経由で脱獄
bash-5.1# cd /proc/1/cwd
bash-5.1# echo *
aur bak bin boot dev etc home lib lib64 mnt nfs opt pkg proc root run sbin srv sys tmp usr var
bash-5.1# 

ファイルディスクリプタ

そのプロセスがどんなファイルを開いているか、確認できます。

luna:~ % ls -l /proc/self/fd/
合計 0
lrwx------ 1 masm masm 64  926 19:54 0 -> /dev/pts/1
lrwx------ 1 masm masm 64  926 19:54 1 -> /dev/pts/1
lrwx------ 1 masm masm 64  926 19:54 2 -> /dev/pts/1
lr-x------ 1 masm masm 64  926 19:54 3 -> /proc/133657/fd/
luna:~ % 

/dev/pts/1 というのは擬似端末です。別のプロセスとつながっています。 今回の場合は、self は ls コマンドですから、出力先は端末エミュレータなので、 /dev/pts/1 はその端末エミュレータにつながっています。

では、pipe を作ります。

luna:~ % cat | cat

PID を確認して、/proc/PID/fd/ を確認します。

luna:~ % ps auxww | grep ' cat$'
masm      140343  0.0  0.0   7980   780 pts/1    S+   21:16   0:00 cat
masm      140344  0.0  0.0   7980   788 pts/1    S+   21:16   0:00 cat
luna:~ % ls -l /proc/140343/fd/
合計 0
lrwx------ 1 masm masm 64  926 21:18 0 -> /dev/pts/1
l-wx------ 1 masm masm 64  926 21:18 1 -> 'pipe:[344223]'
lrwx------ 1 masm masm 64  926 21:18 2 -> /dev/pts/1
luna:~ % ls -l /proc/140344/fd/
合計 0
lr-x------ 1 masm masm 64  926 21:18 0 -> 'pipe:[344223]'
lrwx------ 1 masm masm 64  926 21:18 1 -> /dev/pts/1
lrwx------ 1 masm masm 64  926 21:18 2 -> /dev/pts/1
luna:~ % 

0 や 1 が変なものを指してますね。 pipe らしいです。

0 が標準入力、1 が標準出力です。 なので、

  • 左側の cat: 出力先が pipe: PID=140343
  • 右側の cat: 入力元が pipe: PID=140344

です。

この fd に書き込むとどうなるのでしょうか?

echo 1 > /proc/140343/fd/1

cat してる端末に変化がありました。

luna:~ % cat | cat
1

なんと、pipe に割り込めました。 確かに /proc/PID/fd/ は実体を指しているようです。

こうなると、socket でもできるかもしれない、と想像が働きます。 ssh の connection に割り込んでみます。

luna:~ % ls -al /proc/138500/fd/
合計 0
dr-x------ 2 masm masm  0  926 21:11 ./
dr-xr-xr-x 9 masm masm  0  926 21:10 ../
lrwx------ 1 masm masm 64  926 21:11 0 -> /dev/pts/1
lrwx------ 1 masm masm 64  926 21:11 1 -> /dev/null
lrwx------ 1 masm masm 64  926 21:11 2 -> /dev/pts/1
lrwx------ 1 masm masm 64  926 21:11 3 -> 'socket:[338439]'
lrwx------ 1 masm masm 64  926 21:11 4 -> /dev/pts/1
lrwx------ 1 masm masm 64  926 21:11 5 -> /dev/pts/1
lrwx------ 1 masm masm 64  926 21:11 6 -> /dev/pts/1
luna:~ % echo foo > /proc/138500/fd/3
zsh: そのようなデバイスやアドレスはありません: /proc/138500/fd/3

あら残念、さすがに無理かー

まとめ

procfs を使って、いろいろ遊んでみました。

もしかすると、何か便利な場面はあるかもしれませんね。 /proc/PID/cwd の例で示したように、サーバが変な挙動をしている場合の調査には使えるかもしれません。

ではまた!