Logo address

SYMLINK と格闘

2017/04/22
2021/11/05 追加 (プロセス閉じ込め)

SYMLINK

unix は幾つかの醜い面を持っている。(もちろん Windows に比べると遥かに美しい世界ではある。)
その中の1つが SYMLINK である。例えば僕の Mac (unix の1変種である)では

-bash$ ls -l /usr/bin/vi
lrwxr-xr-x  1 root  wheel  3 11 18  2014 /usr/bin/vi -> vim
-bash$

図1: vi

SYMLINK が相手だとアウトプットのフィールドがイレギュラーである。このようなイレギュラーなアウトプットが含まれていると、これを何かのプログラムのインプットとして使いたい時に面倒なことになる。unix の良さは、プログラムのアウトプットが、他のプログラムのインプットとなるように設計しやすい点にあるのだが、これを台無しにするのである。

このアウトプットは /usr/bin/vi の実体は vim であることを伝えている。従って /usr/bin/vi について知りたければ vim を見なくてはならない。この vim の場所は、SYMLINK が作成された vi の場所と同じである。そこで vim の属性を

-bash$ ls -l /usr/bin/vim
-rwxr-xr-x  1 root  wheel  1530240  6 24  2016 /usr/bin/vim
-bash$

図2: vim

で手に入れることができる。これは舞台裏における vi の顔である。表舞台では、こうした仕掛けが隠されて vi は一人前の役者となる。表舞台の様子は lsL オプションで表示される。

-bash$ ls -lL /usr/bin/vi
-rwxr-xr-x  1 root  wheel  1530240  6 24  2016 /usr/bin/vi
-bash$

図3: vi

仕組みは簡単だ。/usr/bin/vi の実体は小さなファイルで、この場合には

ls -l /usr/bin/vi
の表示(図1)をみればわかるように、たった 3B、ここには "vim" と書かれているだけだ。ここで表示された
/usr/bin/vi -> vim
がそのことを表している。SYMLINK は案内標識で、vim を見なさいと言っている。/usr/bin に置かれた案内標識なので、/usr/bin の中で vim を探すことになる。

SYMLINK は仕組みから言えばファイルなのだが、以下では単にファイルと言えば SYMLINK を含めないことにする。特性があまりにも他のファイルと異なっているからである。

SYMLINK の基本的な作り方は

ln -s bar foo
のようなものである1foo にはパス名を書く。絶対パスでもよいし、作業場所からの相対パスでもよい。他方 bar は何でもよい。この部分には案内標識に書き込む内容を書く。例えば "I love you" だって許されるが、間違った案内標識は混乱を招くので、十分に気を付ける必要がある。

観客が見る世界、すなわち、表舞台で大切な点は次の通りである: /usr/bin/vi が SYMLINK であることが隠され、ファイルと同じ扱いを受けている。すなわち、/usr/bin/vi をコマンドとみなしても不都合は生じない。

表舞台では SYMLINK の持つ醜さを隠してしまう。表舞台は幸せな世界のように見えるが... ここは仮装舞台であって、奇妙奇天烈な舞台になり得るのである。

案内標識を辿ると、先にはまた案内標識があり、それに従って辿るとまた案内標識があり... 結局最終的には次の4つのケースがあり得る。

(a) ファイルに行き着く
(b) ディレクトリに行き着く
(c) 何も無い (以下 broken link と言う)
(d) 循環している (以下 cyclic link と言う)


注1: ln は多数のオプションを備えているが、これ以外の形式は必要の無いものである。ところで筆者は時々 barfoo の関係が分からなくなる。そこで link の持つ英語の語感をこの際に整理しておく。コマンドを英語に直すと、
link bar to foo
である。従って英文マニュアルでは bar を linked file、foo を linked-to file と表現している。日本人には分かりにくいが barfoo の関係は対等ではなく、ちょうど
add bar to foo
の関係と似ているのではないかと思える。だから、例えば
link a file to the directory
と言うことがあっても
link a directory to the file
とは言わないのであろう。
この link の持っている語感は s オプションのない link に現れている。


いくつかの例

test1

正しく作られた SYMLINK で、ケース(a)の例である。

-bash$ ls -l
total 32
lrwxr-xr-x  1 arisawa  staff     5  4 17 11:55 file1 -> file2
lrwxr-xr-x  1 arisawa  staff     5  4 17 12:39 file2 -> file3
-rw-r--r--  1 arisawa  staff  2231  4 17 12:39 file3
-rw-r--r--@ 1 arisawa  staff    76  4 17 12:40 memo.txt
-bash$ ls -Ll
total 32
-rw-r--r--  1 arisawa  staff  2231  4 17 12:39 file1
-rw-r--r--  1 arisawa  staff  2231  4 17 12:39 file2
-rw-r--r--  1 arisawa  staff  2231  4 17 12:39 file3
-rw-r--r--@ 1 arisawa  staff    76  4 17 12:40 memo.txt
-bash$

file1file2file3 を表している正しいリンクである。

test2

簡単な cyclic link

-bash$ ls -l
total 24
lrwxr-xr-x  1 arisawa  staff   5  4 17 11:55 file1 -> file2
lrwxr-xr-x  1 arisawa  staff   5  4 17 11:53 file2 -> file1
-rw-r--r--@ 1 arisawa  staff  68  4 17 12:00 memo.txt
-bash$ ls -Ll
total 24
-rw-r--r--@ 1 arisawa  staff  68  4 17 12:00 memo.txt
-bash$
ls コマンドは L オプションの下では cyclick link になっているファイルは表示していない。これは正しい考え方である。

test3

長い cyclic link の例

-bash$ ls -l
total 32
lrwxr-xr-x  1 arisawa  staff    5  4 17 11:55 file1 -> file2
lrwxr-xr-x  1 arisawa  staff    5  4 17 12:39 file2 -> file3
lrwxr-xr-x  1 arisawa  staff    5  4 17 13:05 file3 -> file1
-rw-r--r--@ 1 arisawa  staff  114  4 17 13:05 memo.txt
-bash$ ls -Ll
total 32
-rw-r--r--@ 1 arisawa  staff  114  4 17 13:05 memo.txt
-bash$

ls は正しく働いている。

test4

broken link の例

-bash$ ls -l
total 32
lrwxr-xr-x  1 arisawa  staff    5  4 18 13:24 file1 -> file2
lrwxr-xr-x  1 arisawa  staff    5  4 18 13:24 file2 -> file3
lrwxr-xr-x  1 arisawa  staff    6  4 18 13:25 file3 -> blabla
-rw-r--r--@ 1 arisawa  staff  114  4 18 13:21 memo.txt
-bash$ ls -Ll
total 32
-rw-r--r--@ 1 arisawa  staff  114  4 18 13:21 memo.txt
-bash$

lsL オプションの下では broken link は表示しない。これは正しい考え方である。

test5

次に cyclick link ではないが、下図に示すようにディレクトリを循環的に繋ぐリンクを調べる。

図4: 循環ディレクトリ

これはケース(b)の例であるが、素直ではない。ディレクトリへのリンクの場合には、このように意地悪くリンクを貼ると、表舞台ではあたかも無限に深いディレクトリであるかのように見せることが可能になる。

-bash$ ls -l
total 8
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 dir1
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 dir2
-rw-r--r--@ 1 arisawa  staff  123  4 17 18:36 memo.txt
-bash$ ls -l dir1
total 8
-rw-r--r--  1 arisawa  staff  0  4 18 16:15 bar1
-rw-r--r--  1 arisawa  staff  0  4 18 16:15 foo1
-rw-r--r--  1 arisawa  staff  0  4 18 16:15 hoge1
lrwxr-xr-x  1 arisawa  staff  7  4 17 18:36 ln1 -> ../dir2
-bash$ ls -l dir2
total 8
-rw-r--r--  1 arisawa  staff  0  4 18 16:15 bar2
-rw-r--r--  1 arisawa  staff  0  4 18 16:15 foo2
-rw-r--r--  1 arisawa  staff  0  4 18 16:15 hohe2
lrwxr-xr-x  1 arisawa  staff  7  4 17 18:36 ln2 -> ../dir1
-bash$ ls -lL dir1
total 0
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 bar1
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 foo1
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 hoge1
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 ln1
-bash$ ls -lL dir2
total 0
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 bar2
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 foo2
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 hohe2
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 ln2
-bash$ ls -lL dir1/ln1
total 0
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 bar2
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 foo2
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 hohe2
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 ln2
-bash$ ls -lL dir2/ln2
total 0
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 bar1
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 foo1
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 hoge1
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 ln1
-bash$ ls -lL dir1/ln1/ln2/ln1/ln2
total 0
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 bar1
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 foo1
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 hoge1
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 ln1
-bash$

ツールの現状

ディレクトリを隈なく調べたいことがしばしば発生して、それに役に立つツールが存在する。例えば R オプションを添えた lsfind などである。しかし、SYMLINK の下ではそうしたツールの作成が難しくなる。特に難しくなるのは L オプション(follow link option)をつけた場合である。
こうしたツールの設計には
(a) 表示の欠落を発生させない
(b) 無限ループに陥いらない
(c) 同じファイルを余剰に表示しない
などの要求事項が考えられる。

欠落を発生させるツールはそもそも役に立たない。従って (a) は当然である。(b) の要求はいわばツールが暴走しないための保護である。安全性が確認されている環境、あるいは使い方の下では
(b) の対策を取らなくてもよいであろう。意地の悪い SYMLINK の下でも (c) の要求を満たす既存のものは僕は知らない。そもそも無限ループに陥るような SYMLINK を張る方が悪いのであるから、(c) に関しては正しい SYMLINK の下で満たされていれば良しとして割り切っても良いと思える。

実験環境

ディレクトリ test1 から test5 をどのように処理できたかを、lsfind について調べる。
なお、正しく SYMLINK が貼られているのは test1 だけで、
test2test3 は cyclic link、test4 は broken link になっている。
test5 はディレクトリへのリンクで、リンクをフォローしていくと、無限に深いディレクトリ(一種の無限ループ)になっている。

SYMLINK をフォローさせない場合には、いずれも正しく処理している。

ls

-bash$ ls -RLl test[1-5]
test1:
total 32
-rw-r--r--  1 arisawa  staff  2231  4 17 12:39 file1
-rw-r--r--  1 arisawa  staff  2231  4 17 12:39 file2
-rw-r--r--  1 arisawa  staff  2231  4 17 12:39 file3
-rw-r--r--@ 1 arisawa  staff    76  4 17 12:40 memo.txt

test2:
total 24
-rw-r--r--@ 1 arisawa  staff  68  4 17 12:00 memo.txt
ls: file1: Too many levels of symbolic links
ls: file2: Too many levels of symbolic links

test3:
total 32
-rw-r--r--@ 1 arisawa  staff  114  4 17 13:05 memo.txt
ls: file1: Too many levels of symbolic links
ls: file2: Too many levels of symbolic links
ls: file3: Too many levels of symbolic links

test4:
total 32
-rw-r--r--@ 1 arisawa  staff  114  4 18 13:21 memo.txt
ls: file1: No such file or directory
ls: file2: No such file or directory
ls: file3: No such file or directory

test5:
total 8
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 dir1
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 dir2
-rw-r--r--@ 1 arisawa  staff  123  4 17 18:36 memo.txt

test5/dir1:
total 0
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 bar1
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 foo1
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 hoge1
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 ln1

test5/dir1/ln1:
total 0
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 bar2
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 foo2
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 hohe2
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 ln2
ls: ln2: directory causes a cycle

test5/dir2:
total 0
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 bar2
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 foo2
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 hohe2
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 ln2

test5/dir2/ln2:
total 0
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 bar1
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 foo1
-rw-r--r--  1 arisawa  staff    0  4 18 16:15 hoge1
drwxr-xr-x  6 arisawa  staff  204  4 18 16:15 ln1
ls: ln1: directory causes a cycle
-bash$

正しく cyclic link を防いでいるが、test5 では同じファイルを多重に表示している。

find

find は SYMLINK をフォローするオプションとして LH を持つ。

-bash$ find -L test[1-5]
test1
test1/file1
test1/file2
test1/file3
test1/memo.txt
test2
test2/file1
test2/file2
test2/memo.txt
test3
test3/file1
test3/file2
test3/file3
test3/memo.txt
test4
test4/file1
test4/file2
test4/file3
test4/memo.txt
test5
test5/dir1
test5/dir1/bar1
test5/dir1/foo1
test5/dir1/hoge1
test5/dir1/ln1
test5/dir1/ln1/bar2
test5/dir1/ln1/foo2
test5/dir1/ln1/hohe2
test5/dir1/ln1/ln2
test5/dir2
test5/dir2/bar2
test5/dir2/foo2
test5/dir2/hohe2
test5/dir2/ln2
test5/dir2/ln2/bar1
test5/dir2/ln2/foo1
test5/dir2/ln2/hoge1
test5/dir2/ln2/ln1
test5/memo.txt
-bash$

find は何故か cyclick link や broken link も表示している。test5 では無限ループを防いだが、ls と同様に同じファイルを多重に表示している。

lr

lr は筆者の自作ツールで、もともとは plan9 用に作成したのであるが、今回 unix 版で SYMLINK をサボートした1。この記事はその経験に基づいている。

-bash$ lr -l test[1-5]
drwxr-xr-x arisawa  staff             0 2017/04/17 12:40:02 test1
lrw-r--r-- arisawa  staff          2231 2017/04/17 12:39:48 test1/file1
lrw-r--r-- arisawa  staff          2231 2017/04/17 12:39:48 test1/file2
-rw-r--r-- arisawa  staff          2231 2017/04/17 12:39:48 test1/file3
-rw-r--r-- arisawa  staff            76 2017/04/17 12:40:02 test1/memo.txt
drwxr-xr-x arisawa  staff             0 2017/04/17 12:00:38 test2
-rw-r--r-- arisawa  staff            68 2017/04/17 12:00:38 test2/memo.txt
drwxr-xr-x arisawa  staff             0 2017/04/17 13:05:32 test3
-rw-r--r-- arisawa  staff           114 2017/04/17 13:05:01 test3/memo.txt
drwxr-xr-x arisawa  staff             0 2017/04/18 13:25:00 test4
-rw-r--r-- arisawa  staff           114 2017/04/18 13:21:59 test4/memo.txt
drwxr-xr-x arisawa  staff             0 2017/04/17 18:36:56 test5
drwxr-xr-x arisawa  staff             0 2017/04/18 16:15:17 test5/dir1
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:17 test5/dir1/bar1
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:17 test5/dir1/foo1
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:17 test5/dir1/hoge1
lrwxr-xr-x arisawa  staff             0 2017/04/18 16:15:50 test5/dir1/ln1
drwxr-xr-x arisawa  staff             0 2017/04/18 16:15:50 test5/dir2
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:50 test5/dir2/bar2
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:50 test5/dir2/foo2
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:50 test5/dir2/hohe2
lrwxr-xr-x arisawa  staff             0 2017/04/18 16:15:17 test5/dir2/ln2
-rw-r--r-- arisawa  staff           123 2017/04/17 18:36:56 test5/memo.txt
-bash$

SYMLINK の表示法が ls と異なっていることに注意しよう。すなわち、SYMLINK 自体の属性ではなく、リンク先のファイルの属性が表示されている。

SYMLINK をフォローさせると次の結果になる。

-bash$ lr -lL test[1-5]
drwxr-xr-x arisawa  staff             0 2017/04/17 12:40:02 test1
-rw-r--r-- arisawa  staff          2231 2017/04/17 12:39:48 test1/file1
-rw-r--r-- arisawa  staff          2231 2017/04/17 12:39:48 test1/file2
-rw-r--r-- arisawa  staff          2231 2017/04/17 12:39:48 test1/file3
-rw-r--r-- arisawa  staff            76 2017/04/17 12:40:02 test1/memo.txt
drwxr-xr-x arisawa  staff             0 2017/04/17 12:00:38 test2
# test2/file1: broken or cyclic link
# test2/file2: broken or cyclic link
-rw-r--r-- arisawa  staff            68 2017/04/17 12:00:38 test2/memo.txt
drwxr-xr-x arisawa  staff             0 2017/04/17 13:05:32 test3
# test3/file1: broken or cyclic link
# test3/file2: broken or cyclic link
# test3/file3: broken or cyclic link
-rw-r--r-- arisawa  staff           114 2017/04/17 13:05:01 test3/memo.txt
drwxr-xr-x arisawa  staff             0 2017/04/18 13:25:00 test4
# test4/file1: broken or cyclic link
# test4/file2: broken or cyclic link
# test4/file3: broken or cyclic link
-rw-r--r-- arisawa  staff           114 2017/04/18 13:21:59 test4/memo.txt
drwxr-xr-x arisawa  staff             0 2017/04/17 18:36:56 test5
drwxr-xr-x arisawa  staff             0 2017/04/18 16:15:17 test5/dir1
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:17 test5/dir1/bar1
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:17 test5/dir1/foo1
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:17 test5/dir1/hoge1
drwxr-xr-x arisawa  staff             0 2017/04/18 16:15:50 test5/dir1/ln1
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:50 test5/dir1/ln1/bar2
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:50 test5/dir1/ln1/foo2
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:50 test5/dir1/ln1/hohe2
# test5/dir1/ln1/ln2: cyclic directory link
drwxr-xr-x arisawa  staff             0 2017/04/18 16:15:50 test5/dir2
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:50 test5/dir2/bar2
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:50 test5/dir2/foo2
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:50 test5/dir2/hohe2
# test5/dir2/ln2: cyclic directory link
-rw-r--r-- arisawa  staff           123 2017/04/17 18:36:56 test5/memo.txt
-bash$

test5 については、余剰表示の個数が lslr に比べて減少している。他方、表示結果は dir1dir2 について非対称である。この非対称性は採用したアルゴリズムの結果である。

このアルゴリズムは以下のようなものである。
ディレクトリにリンクする SYMLINK で出会ったら、置かれているディレクトリをマークする。SYMLINK の先が既にマークされていれば、その SYMLINK は辿らない。

lrls と同様に broken link や cyclic link は表示しない。 代わりに警告メッセージを出すのみである。

余剰表示を完全に防ぐには、辿ったすべてのディレクトリをマークしなくてはならなくなるであろう。これは保護にしては大袈裟すぎる。

一応、余剰表示を完全に防ぐオプション R (no redundant output) を入れてみた。次はそのアウトプットである。

-bash$ lr -lLR test5
drwxr-xr-x arisawa  staff             0 2017/04/17 18:36:56 test5
drwxr-xr-x arisawa  staff             0 2017/04/18 16:15:17 test5/dir1
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:17 test5/dir1/bar1
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:17 test5/dir1/foo1
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:17 test5/dir1/hoge1
drwxr-xr-x arisawa  staff             0 2017/04/18 16:15:50 test5/dir1/ln1
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:50 test5/dir1/ln1/bar2
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:50 test5/dir1/ln1/foo2
-rw-r--r-- arisawa  staff             0 2017/04/18 16:15:50 test5/dir1/ln1/hohe2
# test5/dir1/ln1/ln2: cyclic directory link
drwxr-xr-x arisawa  staff             0 2017/04/18 16:15:50 test5/dir2
-rw-r--r-- arisawa  staff           123 2017/04/17 18:36:56 test5/memo.txt
-bash$

どのファイルも1度だけ表示されている。

R オプションは廃止されるかも知れない。自分はオプションを増やすのを好まない。大切なのは間違った SYMLINK に対して警告メッセージを出し、間違いが訂正されるのに役に立つことであって、間違いをそのままにして余剰を防ぐことではない。その点で find は失格である。


注1: lr のソースコードは http://p9.nyx.link/netlib/cmd/lr/ に置かれている。ただしコンパイルには Plan9port が必要である。
補注: 現在(2021-10-24)の版はバグを持っているので、修正版を近々公開する予定である。公開しました(2021-10-28)

プログラミング

基礎知識

ファイルに対する ls コマンドの例

-bash$ ls -l /usr/bin/vim
-rwxr-xr-x  1 root  wheel  1530240  6 24  2016 /usr/bin/vim
を考える。このうち、
-rwxr-xr-x  1 root  wheel  1530240  6 24  2016
の部分は、(sb を stat 構造体として)
stat("/usr/bin/vim",&sb)
によって得られる。あるいは、(lsb を stat 構造体として)
lstat("/usr/bin/vim",&lsb)
としても全く同じ結果が得られる1

SYMLINK の場合には事情が異なる。stat(path,&sb)lstat(path,&lsb) では得られるものが異なっている。例を示す。/usr/bin/vi は SYMLINK である。その場合、命令

lstat("/usr/bin/vi",&lsb)
によって、
-bash$ ls -l /usr/bin/vi
lrwxr-xr-x  1 root  wheel  3 11 18  2014 /usr/bin/vi -> vim

lrwxr-xr-x  1 root  wheel  3 11 18  2014
の部分が stat 構造体 lsb に与えられる。他方
stat("/usr/bin/vi",&sb)
の結果、stat 構造体 sb には、/usr/bin/vi がリンクされている /usr/bin/vim の stat 情報
stat("/usr/bin/vim",&sb)
が与えられる。その結果は lsL オプションで確認できる。
-bash$ ls -lL /usr/bin/vi
-rwxr-xr-x  1 root  wheel  1530240  6 24  2016 /usr/bin/vi

なお "lstat" の "l" は link の意味である。


注1: /dev/ の中のファイルには、SYMLINK でないにも関わらず、stat(path,&sb)lstat(path,&lsb) で得られる sblsb が異なるものが存在する。例えば MacOS の場合 /dev/io8log* がそうである。これらは character device であるが、他の character device では両者は一致している。

stat() と lstat()

筆者は Plan9 の愛好家であって unix のプログラミングには慣れていない。久しぶりに unix でプログラムをしたら色々な発見があった。何しろ Plan9 には SYMLINK は存在しない。

SYMLINK は構造的には単なるテキストファイルである。パスを path とすると、SYMLINK か否かの判定は次のようにすればよい。

# include <sys/stat.h>
char *path;
struct stat lsb;
...
	if(lstat(path,&lsb)&lt;0){
		/* stat 情報が取れない (ファイルが存在しない) */
		...
	}
	else{
		if(S_ISLNK(lst->st_mode)){
			/* then path is SYMLINK */
			...
		}
		else{
			/* then path is a file or a dir */
			...
		}
	}

ここに S_ISLNKsys/stat.h

#define	S_ISLNK(m)	(((m)&amp;S_IFMT) == S_IFLNK)
として定義されているマクロで、SYMLINK であることを判定する。

path が SYMLINK であれば

# include <sys/stat.h>
char *path;
struct stat sb;
...
	if(stat(path,&sb)&lt;0){
		/* stat 情報が手に入らなかった。
		 * 原因は path が存在しない、
		 * あるいは broken or cyclic SYMLINK など。
		 * sb はそのまま */
		...
	}
	else{
		/* この場合 SYMLINK チェーンを辿った先の
	 	 *  file や dir の sb が設定される  */
		if(S_ISDIR(st->st_mode)){
			/* dir である */
			...
		}
		else{
			/* file の類 */
			...
		}
	}

stat 構造体には ls -l コマンドで表示されている内容(ただしファイル名を除く)が含まれている。MacOS の場合には次のようになっている。

     struct stat { /* when _DARWIN_FEATURE_64_BIT_INODE is defined */
         dev_t           st_dev;           /* ID of device containing file */
         mode_t          st_mode;          /* Mode of file (see below) */
         nlink_t         st_nlink;         /* Number of hard links */
         ino_t           st_ino;           /* File serial number */
         uid_t           st_uid;           /* User ID of the file */
         gid_t           st_gid;           /* Group ID of the file */
         dev_t           st_rdev;          /* Device ID */
         struct timespec st_atimespec;     /* time of last access */
         struct timespec st_mtimespec;     /* time of last data modification */
         struct timespec st_ctimespec;     /* time of last status change */
         struct timespec st_birthtimespec; /* time of file creation(birth) */
         off_t           st_size;          /* file size, in bytes */
         blkcnt_t        st_blocks;        /* blocks allocated for file */
         blksize_t       st_blksize;       /* optimal blocksize for I/O */
         uint32_t        st_flags;         /* user defined flags for file */
         uint32_t        st_gen;           /* file generation number */
         int32_t         st_lspare;        /* RESERVED: DO NOT USE! */
         int64_t         st_qspare[2];     /* RESERVED: DO NOT USE! */
     };

MacOS の stat 構造体。OS によって異なる。

st_mode の各 bit の意味は

     #define S_IFMT 0170000           /* type of file */
     #define        S_IFIFO  0010000  /* named pipe (fifo) */
     #define        S_IFCHR  0020000  /* character special */
     #define        S_IFDIR  0040000  /* directory */
     #define        S_IFBLK  0060000  /* block special */
     #define        S_IFREG  0100000  /* regular */
     #define        S_IFLNK  0120000  /* symbolic link */
     #define        S_IFSOCK 0140000  /* socket */
     #define        S_IFWHT  0160000  /* whiteout */
     #define S_ISUID 0004000  /* set user id on execution */
     #define S_ISGID 0002000  /* set group id on execution */
     #define S_ISVTX 0001000  /* save swapped text even after use */
     #define S_IRUSR 0000400  /* read permission, owner */
     #define S_IWUSR 0000200  /* write permission, owner */
     #define S_IXUSR 0000100  /* execute/search permission, owner */

16bit で定義されているのだね〜
unix が生まれたのは、こんな時代だから...
bit ごとに独立した意味を与えていないことが、判定を(少しだけ)面倒にしている。

opendir() と readdir()

ディレクトリのパス path を与えて、その中に含まれるファイル名を知る。

#include <dirent.h>
char *path;
DIR *d;
struct dirent *de;
...
	d = opendir(path);
	if(d == NULL){
		/* error */
		...
	}
	else{
		while ((de = readdir(d)) != NULL) {
			printf("%s\n",de->->d_name);/* or something else */
			...
		}
		/* dont't free(de) */
	}
	closedir(d);

ここで使われている構造体の内容を次に示す。

typedef struct {
	int	__dd_fd;	/* file descriptor associated with directory */
	long	__dd_loc;	/* offset in current buffer */
	long	__dd_size;	/* amount of data returned */
	char	*__dd_buf;	/* data buffer */
	int	__dd_len;	/* size of data buffer */
	long	__dd_seek;	/* magic cookie returned */
	long	__dd_rewind;	/* magic cookie for rewinding */
	int	__dd_flags;	/* flags for readdir */
	__darwin_pthread_mutex_t __dd_lock; /* for thread locking */
	struct _telldir *__dd_td; /* telldir position recording */
} DIR;

struct dirent { /* when _DARWIN_FEATURE_64_BIT_INODE is defined */
	ino_t      d_fileno;     /* file number of entry */
	__uint64_t d_seekoff;    /* seek offset (optional, used by servers) */
	__uint16_t d_reclen;     /* length of this record */
	__uint16_t d_namlen;     /* length of string in d_name */
	__uint8_t  d_type;       /* file type, see below */
	char    d_name[1024];    /* name must be no longer than this */
};

MacOS の DIR 構造体と dirent 構造体。OS によって異なる。

Linux の場合には d_namlen は dirent 構造体に含まれていない。(困らない)

readlink()

SYMLINK チェーンの先がファイルであれば、この path はファイルと同等の、ディレクトリであればディレクトリと同等の、扱いを受けるのである。
従って SYMLINK ファイルの中身を覗くには open(path,...) では覗けない。(リンク先のファイルが open されるから)

#include <unistd.h>

int n;
char *path;
char buf[256];/* 適当な十分な大きさのサイズで */
...
	n = readlink(path,buf,sizeof(buf))
	if(n&lt;0){
		/* error */
		...
	}
	buf[n] = 0;
	printf("%s\n",buf); /* or something */

st_ino

再巡回を避けるために、巡回済(あるいは巡回中)のディレクトリは必要に応じてリストに登録する。リストに登録する情報はディレクトリをユニークに特徴付けるものでなくてはならない。巡回中のプロセスから見えるディレクトリのパスではダメである。ここは一種の仮想空間であり、本来のパス名が分からないからである。では何を使うか?
Plan9 では qid.path なるものをその目的のために使っている。path と名前が付いているが、ディレクトリをユニークに識別する数字である。Russ Cox1 は Plan9 のソフトウェアを unix に移植するにあたって、qid.path の代用として stat 構造体の st_ino を採用している。

Plan9 には SYMLINK なるものは存在しない。システムのメンテ作業においては木構造でないと制御しにくいからである。しかし他方では木構造を基にして変幻自在な仮想空間をアプリケーションに提供する仕組みを持っている。そのためにファイルを識別する情報を必要としているのである。


注1: Russ Cox は Plan9 と unix に精通している、世界的なトッププログラマーの一人である。

プロセス閉じ込め

2021/11/05

プロセスとは実行中のプログラムのことである。

ps aux
を実行してみれば、非常に多くのプロセスが背後に蠢いているのが理解できるであろう。
Web のサーバーなどで下手な CGI プログラムを作ると、見られたくないファイルを見せることになる。安全な運用には、プロセスが限られた名前空間(ファイルやディレクトリの名前が作る空間)の中で実行させることが求められる。僕が設計した Web のサーバー Pegasus は、そのようなコンセプトの下で作られているが、unix や Linux の下ではなかなかその方向に進まない。しかし最近はその方向が見えてきた。ここではこの問題を採り上げる。

SYMLINK とプロセス閉じ込め

SYSLINK は「プロセス閉じ込め」とは極めて相性が悪い。unix や Linux では「プロセス閉じ込め」の基本的な技術として chroot がある。まずここから始める。

chroot とはコマンドの名前であり、foo をディレクトリの名前であるとする。このコマンドは

cd foo
sudo chroot .
のように実行する。foo をうまく作ると foo の中に閉じ込められたプロセスを生成できる。いい加減に foo を作ると、(FreeBSD の場合には)
chroot: /bin/csh: No such file or directory
と怒られる。このメッセージは foo/bin/csh が無いと言っている。なぜ csh かと言えば FreeBSD では root プロセスは csh を使うからである1。そこで csh を foo の中に追加する。そこで
mkdir bin
cp /bin/csh bin
を実行する。なぜコピーなのか? 余計なディスク容量を食うではないか? symlink ではいけないのか? ダメである。ダメな理由は、我々は foo の下にプロセスを閉じ込める檻を作る方針であり、/bin/csh は檻の外にあるからである。

foo/bin/csh を作って再度

sudo chroot .
を実行すると、今度は
ELF interpreter /libexec/ld-elf.so.1 not found, error 2
と言われる。そしてまた追加する。今度は
ld-elf.so.1: Shared object "libncursesw.so.9" not found, required by "csh"
と言われた。この作業が延々と続く。そこで結局どうしたらよいの? 面倒だから、過剰でもよいから... 結局
bsd$ mkdir bin lib libexec
bsd$ cp /bin/csh bin
bsd$ cp /lib/*.so.* lib
bsd$ cp /libexec/ld-elf.so.1 libexec
bsd$ sudo chroot .
#		# OK. However we can do almost nothing
# exit
bsd$
root のプロセス cshfoot の中に入ることに成功したが、事実上何もできない。コマンドのプログラムが入っていないから。檻の外のプログラムは実行できない。檻の外はそもそも見えないのである。楽しくない。はじめから /bin のプログラムぐらいは foo/bin にコピーしておけば、もう少し楽しかったのに... (読者の課題とする)

Linux の場合、chroot コマンドのエラーメッセージを頼りに foo の内容を増やそうとしても上手くいかない。エラーメッセージが不親切で、何が不足しているのかを知らせてくれない。単に /bin/bash が無いと言われるだけである。この場合 ldd コマンドが役に立つ2

maia$ cd $t
maia$ sudo chroot .
chroot: failed to run command ‘/bin/bash’: No such file or directory
maia$ ldd /bin/bash
	linux-vdso.so.1 (0x00007ffe2431f000)
	libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f0d058fb000)
	libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0d058f5000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0d05703000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f0d05a70000)
maia$ mkdir -p lib/x86_64-linux-gnu lib64
maia$ cp /lib/x86_64-linux-gnu/* lib/x86_64-linux-gnu
maia$ cp /lib64/ld-linux-x86-64.so.2 lib64
maia$ sudo chroot .
bash: warning: setlocale: LC_ALL: cannot change locale (ja_JP.UTF-8)
bash-5.0#

ここでも、ダイナミックライブラリは過剰である。

目的に応じて必要なプログラムが異なる。そのためのツールもまた存在するようである。

macOS は FreeBSD からコマンド環境を受け継いでいる。従って僕の現在の環境では chroot も使える。「現在の」と書いたのは、chroot を使ったプログラムをコンパイルすると "deplecated" のメッセージを出すからである。Apple は将来 chroot を廃止するらしい。将来ではなく、もう既に廃止されているかもしれらい。「プロセス閉じ込め」はセキュリティの保持にとって有用な技術である。chroot に代わって、Apple は何かを既に持っているのだろうか? 注目して行きたい。


注1. 本当の csh ではない。実態は csh を拡張した tcsh である。システム起動に必要なスクリプト言語には sh が使われている。また FreeBSD ではエンドユーザーが使うコマンド言語は sh に設定されている。FreeBSD の man csh(1) および man sh(1) を見よ。
注2. ldd の最初の表示 linux-vdso.so.1 に関しては、locate コマンドで調べても、このファイルは表示されない。どうやらファイルシステムの中には存在しないらしい。文献[1]を見よ。

[1] VDSO
https://linuxjm.osdn.jp/html/LDP_man-pages/man7/vdso.7.html

Plan9 bind

檻の中に必要な備品を全て配置するのは資源の無駄遣いである。同じ備品であっても檻の数だけ準備しなくてはならないだけではなく、そんなことをやっていれば、檻をダイナミックに構成できない。
この問題に関して Plan9 は極めて上手にやっている。この手法が FreeBSD や Linux でも(ある程度)使えるようになっているので、それを紹介する。

FreeBSD には mount_nullfs が存在する:

bsd$ ls -l /sbin/mount*
-r-xr-xr-x  1 root  wheel  26304 Apr  9  2021 /sbin/mount
-r-xr-xr-x  1 root  wheel  12768 Apr  9  2021 /sbin/mount_cd9660
-r-xr-xr-x  1 root  wheel  17840 Apr  9  2021 /sbin/mount_fusefs
-r-xr-xr-x  2 root  wheel  18672 Apr  9  2021 /sbin/mount_mfs
-r-xr-xr-x  1 root  wheel  14152 Apr  9  2021 /sbin/mount_msdosfs
-r-xr-xr-x  1 root  wheel  27896 Apr  9  2021 /sbin/mount_nfs
-r-xr-xr-x  1 root  wheel   9248 Apr  9  2021 /sbin/mount_nullfs
-r-xr-xr-x  1 root  wheel  11120 Apr  9  2021 /sbin/mount_udf
-r-xr-xr-x  1 root  wheel  10720 Apr  9  2021 /sbin/mount_unionfs
bsd$

実験をしてみる。$HOME/tmp を既存のディレクトリとする。新たにディレクトリ $HOME/mp を作り

bsd$ cd
bsd$ sudo mount -t nullfs tmp mp
bsd$ ls tmp
...
bsd$ lr mp
...
bsd$

マウントされた mp の内容は tmp と全く同じになっていることが解る。
これは Plan9 の bind コマンドの真似である。ただし Plan9 の方は

この3点を除けば効果は全く同じである。
解せないのはネーミングである。なぜ率直に mount_bind にしなかったのか? 類似のマウントは Linux にも存在し、こちらは率直に
sudo mount --bind foo bar
である1。そして Plan9 の bind と同様に、ファイルからファイルへの bind を許している。

FreeBSD においても mount_bind にしていれば

sudo mount -t bind foo bar
でやって行けたはずである。

調べていくと、FreeBSD の nullfs は Linux の "--bind" とは起源が違うらしく、基本的な違いがあるかも知れない。気がついたら追記する。

なお mount の取り消しは、マウントポイント bar に対して

sudo umount bar
を実行すればよい。

Plan9 流の bind のサポートは、chroot を始めとしたプロセス閉じ込めに革命を齎すだけのポテンシャルを秘めている。FreeBSD の場合には次のようになる:

bsd$ cd $t	# $t is the dir to chroot
bsd$ mkdir bin lib libexec	# one time action
bsd$ sudo mount -t nullfs /bin bin
bsd$ sudo mount -t nullfs /lib lib
bsd$ sudo mount -t nullfs /libexec libexec
bsd$ sudo chroot .
#	# you can do something
# exit

つまり、ファイルのコピーをしなくてもやっていける。Linux の場合も同様である。

地上の世界は檻で閉ざされている。ならば地下空間(カール空間)を通じて外部の参照を(必要に応じて)許しましょうというのが bind の精神である。


注1. ネットで検索すると、"mount -t nullfs" や "mount --bind" の記事は 2011 にまで遡る。この頃から注目されるようになったのだろうか? さらに調べていくと、この nullfs の名前の起源は 4.4 Release(2001) まで遡るらしい。僕の macOS は古く(10.12.6)、サポートされていない。

バグは無いか?

FreeBSD も Linux も、Plan9 の影響を受けて、カーネルに新しい仕組みが徐々に導入されつつある。カーネルは OS の土台である。カーネルのバグは破壊力が大きい。上部の建物を一瞬のうちに吹き飛ばすだけの破壊力を持つ。そこで次の実験をしてみた。

FreeBSD の mount -t nullfs や Linux の mount --bind によるマウントは symlink と似たところがある。すなわち循環可能である。循環マウントすると何が起こるか? 暴走しなければ合格としよう。

どこかに実験用のディレクトリを作る。これを $t とする。
以下の実験で lr は僕の自作のツールで、ディレクトリを再帰的に表示する。unix の ls -lR よりも見やすいと思うので、今回はこれで示す。ディレクトリは "/" で終わる。

実験1 自己循環

bsd$ cd $t
bsd$ mkdir dir
bsd$ mkdir dir/next
bsd$ touch dir/foo dir/bar
bsd$ lr dir
dir/
dir/bar
dir/foo
dir/next/
bsd$ sudo mount -t nullfs dir dir/next
bsd$ lr dir
dir/
dir/bar
dir/foo
dir/next/
dir/next/bar
dir/next/foo
dir/next/next/
bsd$ sudo umount dir/next
bsd$ lr dir
dir/
dir/bar
dir/foo
dir/next/
bsd$ sudo mount -t nullfs dir/next dir
bsd$ lr dir
dir/
bsd$ sudo umount dir
bsd$

オペレーション

mount -t nullfs dir dir/next
は、作用の方向としては symlink
ln -s dir/next dir
に近いが、symlink とは違い、循環しないことが解る。オペレーションの向きを逆にした
mount -t nullfs dir/next dir
は、dir/next が空ディレクトリであるから、dir も空ディレクトリとなっている。

実験2 相互循環

今度はさらに複雑な例として、2つのディレクトリの中での相互循環を挙げる。

bsd$ cd $t
bsd$ mkdir dir1 dir2
bsd$ touch dir1/foo1 dir1/bar1
bsd$ touch dir2/foo2 dir2/bar2
bsd$ mkdir dir1/next1 dir2/next2
bsd$ sudo mount -t nullfs  dir1 dir2/next2
bsd$ sudo mount -t nullfs  dir2 dir1/next1
bsd$ lr dir1 dir2
dir1/
dir1/bar1
dir1/foo1
dir1/next1/
dir1/next1/bar2
dir1/next1/foo2
dir1/next1/next2/
dir2/
dir2/bar2
dir2/foo2
dir2/next2/
dir2/next2/bar1
dir2/next2/foo1
dir2/next2/next1/
bsd$ sudo umount dir1/next1 dir2/next2

実験1と同様循環は起きない。オペレージョンの向きを変えて実験すると

bsd$ sudo mount -t nullfs  dir1/next1 dir2
bsd$ lr -l dir1 dir2
bsd$ lr dir1 dir2
dir1/
dir1/bar1
dir1/foo1
dir1/next1/
dir2/
bsd$ sudo mount -t nullfs  dir2/next2 dir1
mount_nullfs: /usr/home/arisawa/TEST/bind/dir2/next2: No such file or directory
bsd$

最後のオペレーションは当然エラーになり、循環は起きない。

Linux の場合も同様である。また Plan9 の場合も同様である。
エラーになった理由は

mount -t nullfs  dir1/next1 dir2
によって dir2/next2 が隠されたからである。

「循環」と書いたが、bind では循環は起きるはずがないのである。挿し木を想像すればよい。挿される枝はいわゆる木構造をしている。それを継ぎ足しても全体として木構造を保つ。Plan9 の開発者たちは、名前空間が木構造を保つことを極めて重視している。木構造を破る unix のリンク(symlink を含む)を嫌ったのである。

bind の副作用

bind によって、ファイルシステムは本来の姿から他の姿に変る。
従ってファイルシステムのバックアップを採るときには bind は邪魔である。その時には bind を外す必要がある。

Plan9 の場合には、この点についても良くできていて、bind を外さなくても、生の姿を見せる工夫がある。この工夫は Plan9 のキーコンセプトである private namespace に由来する。unix にも private namespace の類似概念が導入される日が来るかもしれないが、今の所その気配は無い。