chmod 755でディレクトリのsetgidが消えない理由
chmod 755 は「所有者は読み書き実行、グループとその他は読み取りと実行」という意味で、多くの人が暗記している。ところが、共有ディレクトリに付けた setgid(2000)を外すつもりで chmod 755 dir を打っても、GNU coreutils の chmod では setgid が残る。0755 と4桁で書いても同じで、消えるのは 00755 と5桁で書いたときである。
この記事では ハシトシステムの chmod 権限計算 のコードを入口に、4桁の数値表記と rwx 表記の対応、ls -l に出る大文字の S と T、ディレクトリの setuid・setgid が数値指定で消えない仕様を、Ubuntu 上で実際に確かめる。
4桁表記の先頭は特殊ビット
パーミッションは12ビットで、下位9ビットが所有者・グループ・その他の rwx(各3ビット)、その上の3ビットが特殊ビットである。8進数で書くと4桁になり、先頭の桁が特殊ビットを表す。
| 値 | 名前 | rwx 表記で置き換わる位置 |
|---|---|---|
| 4 | setuid | 所有者の x の位置 |
| 2 | setgid | グループの x の位置 |
| 1 | sticky | その他の x の位置 |
755 のような3桁は先頭の特殊ビットを省略した形で、ツールも3桁の入力は先頭に 0 を補って4桁として扱っている(index.html から抜粋)。
1 | |
大文字の S と T は「実行ビットがない」印
特殊ビットは rwx 表記に専用の文字を持たず、各組の3文字目(x の位置)を置き換えて表す。ツールの変換関数は次のようになっている。
1 | |
GNU coreutils のマニュアル(ls の「What information is listed」)の説明もこの対応と一致する。
s: setuid または setgid が立っていて、対応する実行ビットも立っているS: setuid または setgid が立っているが、対応する実行ビットは立っていないt: sticky が立っていて、その他の実行ビットも立っているT: sticky が立っているが、その他の実行ビットは立っていない
たとえば 4644 は rwSr--r-- になる。所有者に実行権がないので setuid は働く場面がなく、大文字の S は「設定の取り違えかもしれない」というサインとして読める。1754 の rwxr-xr-T も同様で、sticky はディレクトリで意味を持つビットなので、ファイルに付いた T はまず意図を確認したほうがよい。
ディレクトリの setuid・setgid は数値指定で消えない
GNU coreutils のマニュアル「Directories and the Set-User-ID and Set-Group-ID Bits」は、chmod は(モードで明示しない限り)ディレクトリの setuid・setgid ビットに影響しない、と説明している。これは GNU の拡張で、POSIX はディレクトリのこれらのビットの設定・解除要求を無視する実装も認めている、とも書かれている。
同じページによると、ディレクトリの setuid・setgid を消せるのは次の書き方である。
- 5桁以上の8進数(例:
chmod 00755 D) =付きの数値(例:chmod =755 D)- 記号でビットを名指しする(例:
chmod a-s D、chmod -6000 D)
Linux の man ページ chmod(1)(GNU coreutils 9.11 版)にも、ディレクトリについては明示しない限り setuid・setgid を保持する、と書かれている。一方で通常のファイルにはこの保持は働かず、4755 のファイルに chmod 755 を打つと setuid は外れる(下の実験で確認した)。
つまり「chmod 755 は 0755 にする」と覚えていると、ディレクトリに限って結果が 2755 のまま残る。setgid ディレクトリは中に作ったファイルが親のグループを継ぐ設定なので、外したつもりで外れていないと、グループの継承が続くことになる。
ツールが出力するコマンドは、特殊ビットが0のとき先頭の 0 を落として chmod 755 file の形にする。ファイルにはこれで正しく効くが、setgid 付きのディレクトリを 755 に戻す目的なら、chmod 00755 dir か chmod g-s dir を使う必要がある。
Node.js の fs.chmodSync は数値をそのまま渡す
この「保持」は chmod コマンド側の振る舞いで、プログラムからモードを直接設定する場合は違う結果になる。下の実験では、Node.js の fs.chmodSync(dir, 0o755) は setgid 付きディレクトリを 0755 にした。同じディレクトリに chmod 755 コマンドを打つと 2755 のまま残る。シェルスクリプトと Node.js のデプロイスクリプトで同じ「755」を指定しても、ディレクトリでは結果が食い違う。
実際に試す
Ubuntu 24.04.4 LTS(Linux 6.8)、GNU coreutils 9.4、Node.js v18.19.1 で実行した。依存パッケージは要らない。chmod の特殊ビットは Linux のファイルシステム上で試す必要があり、Windows の NTFS では同じ結果にならない。次の内容を chmod-bits.mjs として保存する。
1 | |
1 | |
出力:
1 | |
読み方:
- 1〜6行目: 特殊ビットは x の位置に出る。実行ビットがあれば小文字(
st)、なければ大文字(ST)。1777は /tmp と同じ設定である。 - 8〜9行目: setgid 付きディレクトリに
chmod 755とchmod 0755を打っても、グループ側の権限だけが変わり(775 → 755)、setgid は残る。 - 10行目: 5桁の
00755で初めて setgid が消える。 - 11行目: Node.js の
fs.chmodSyncは指定した数値どおり0755になる。
ファイルとディレクトリで振る舞いが違うことは、シェルだけでも確かめられる(同じ環境で確認)。
1 | |
1 | |
stat -c '%a' は先頭の特殊ビットが0のとき3桁で表示する。ファイルは chmod 755 で setuid が外れ、ディレクトリは g-s を指定するまで setgid が残る。
まとめ
- パーミッションの4桁表記の先頭は特殊ビット(setuid 4・setgid 2・sticky 1)で、rwx 表記では各組の x の位置に
s/tとして出る。 - 大文字の
S/Tは、特殊ビットは立っているが対応する実行ビットがない状態を表す。 - GNU chmod は、ディレクトリの setuid・setgid を
755や0755では外さない。外すには00755、=755、g-sのように明示する。通常のファイルは755で外れる。 - Node.js の
fs.chmodSyncは指定した数値どおりに設定するので、chmod コマンドとはディレクトリで結果が食い違う。
cron の書式の落とし穴は「cronの日と曜日はOR照合」で、ビット演算でのサブネット計算は「JSのビット演算でサブネット計算」で扱った。
ツール: chmod 権限計算(ハシトシステム)。755 のような数値表記と rwxr-xr-x のような記号表記を相互に変換し、setuid・setgid・sticky を含む4桁表記と chmod コマンドを表示する。処理はブラウザ内で完結する。