はじめに
いずれかのUnix関連の本を読むと、必ずと言っていいほど以下のような文言に遭遇します。例えば私が最近読み始めた「LINUXシステムプログラミング」においてもかなりはじめなほうのファイルとファイルシステムの項で、
Linuxにはすべてのものがファイルである(everything-is-a-file)という思想があります
と書かれていたりしますし、まだソケットの概念のなかったUNIX v6の時代のファイルシステムにおいてもファイルシステムの概要を考慮すると、ほとんどこの思想に近い内容がすでに形作られています。
それで私自身も以下のような図を書いて、「Unixではすべてのオブジェクトがファイル」という説明をしてきました。ソケットも、標準入出力はもちろんのこと、パイプやデバイス(ブロックデバイスやキャラクタデバイス)すらもファイルの扱いになっています。
具体的には、これらはシステムコールで触ると、すべてがファイルディスクリプタ(ファイル記述子)で表現可能となっています。非常にシンプルで斬新な発想です。気になった方はぜひ後述の参考文献『Linuxシステムコール基本リファレンス』で実際にシステムコールを触ってみてください。
ただ、このファイルシステムにはその配下ともいえるところにブロックI/Oシステム(ブロックデバイスサブシステム)が存在してここがバッファとしてデータのキャッシュを確保するようになっているという仕組みがあったり、ファイルシステムにファイルの権限(パーミッション)が付与されていて、ユーザーの権限と比較されていたりとかなり連続的な関係を持っています。この内容は組み合わせが複数あることからやや直感的ではなく、正直この関係性について扱った記事が欲しいなと考えておりました。
そこで今回は、Unixはすべてがファイルという話と併せて、ブロックI/Oデバイスや権限に関する関係性をまとめることとします。
対象読者
基本的にLinux(CentOSやUbuntu)やMacを普段から開発・運用で利用している人なら難なく理解できる内容だと思います。
ただ例のごとく以前書いたUnixの概論にあたる記事から発展させたりそのまま流用した話をするため、もしかしたらこちらをお読みいただくとより理解が促進されるかもしれません。
また開発にしろ運用にしろ、Unixの全体像の把握をしていることを前提として書いています。以下の記事では実際にUnixシェルを触って把握することができるようになっていますのでおすすめです。
UnixファイルシステムとブロックI/Oシステムと権限の関係性
今回記事にしたUnixファイルシステムとブロックI/Oデバイスと権限の関係性をひとつの図でまとめ上げると以下のようになると思います。左側が通常のファイルの一般的な関係で、右側が標準入出力や特殊ファイルの一覧です。
一般的なファイルを取り囲むファイルシステムはディレクトリも含んだもので、木構造を構成します。特にUnixはWindowsと異なり、常にroot(/)ディレクトリが根本になるようにできています。それぞれのファイルやディレクトリの実体に近いものは、この配下に特殊ファイルとなるデバイスファイルと繋がっており、ファイルシステムはその部分を抽象化することで、一般的なファイルたらしめているというわけです。
そしてこのデバイスファイルはブロックI/Oシステムによってつくられており、ここがデバイスに実際にデータを読み書きさせる実行ファイル、デバイスドライバと結び付けられることで、デバイスファイルにただアクセスするだけでデータの読み書きを実現できるようになっています。なおキャラクタデバイスについても限りなくこのブロックデバイスと近い構造になっています。
そしてファイルにおける権限の部分が0XXXと記述されている部分で、これはたとえば0755とかのchmodコマンドを打つときに数字を記述する部分に相当します。この数字はUNIX V6の時代から続く8進数の表記ですが、普段ls -laと打つことが多くてこの数字を見る機会はほぼありません。どうもstatコマンドなどで出せるようですが、特に数字で表記することにこだわる理由もほぼないでしょう。
以下のようにrwxとかの表記が出るはずです。それぞれrがread、wがwrite、xがexecuteに該当します。現在のディレクトリの場所を示す.や上位ディレクトリを示す..もファイル、特にディレクトリの扱いとして存在していることもここでわかります。
% ls -la
drwxr-xr-x@ 5 kurawo staff 160 3 12 08:08 .
drwxr-xr-x 11 kurawo staff 352 1 9 13:09 ..
-rw-r--r-- 1 kurawo staff 3496 3 12 08:11 unixfile.txt
そしてファイルシステムにはページキャッシュと呼ばれるものがあります。これはCPUのハードウェアキャッシュをカーネルによってソフトウェア的に実装したものともいえます。
単にページと呼ぶ場合はメモリ上のデータの最小単位としても扱われています。つまり、このページを複数組み合わせることで変数やファイルになるということです。
このページ自体をメモリかストレージに配置することで、それぞれ実行、および保存に最適な形にすることができます。特に実行の場合は、ページキャッシュとしてメモリ上に事前に確保しておき、再度呼び出されたときにストレージにアクセスするのではなくメモリにアクセスすることでより高速に処理を実現させることができます。
ここ最近のストレージにSSDを利用するコンピュータ、特にiPhoneやiMacにおいてはOSの起動直後でも軽快な動作をするものです。しかしシステムドライブでHDDを利用し、かなり大量のプログラムを実行しなければならない、具体的にはウィンドウシステムなどを動かさなければならない私のかつてのゲーミングWindowsマシン(Core i7第四世代で当時PCに使えるほどの容量のSSDは安くなかった)や高専で使われていた古いiMacについてはOSの起動直後はとてもじゃないですが操作がおぼつかなかったものです。ですが10分経過すればウィンドウシステムも操作がしやすくなり始めます。その理由はOSの最適化もあるでしょうが、ページキャッシュも一役買っていたんじゃないかと振り返ると思います。
ブロックI/Oシステム
ここではデバイスドライバとバッファに関して説明します。
デバイスドライバ
さきほどの図で示した通り、デバイスファイルとデバイスドライバは繋がっています。ここでの読み書きは事実上デバイスドライバに任されていますが、このデバイスドライバの責任は非常に重たいです。ここでのプログラムの品質が低いと、たとえデバイスがファイルとして抽象化されていてカーネルが適切な呼出をしていようが、誤動作の温床となるからです。
例えばカーネルモジュールを内蔵したストレージデバイスドライバであった場合は読み書きができなくなり、それはUnixのカーネル全体の動作を止めることにつながります。
またグラフィックボード用のデバイスドライバであれば、カーネルというよりはユーザー側のプログラムに近い、ウィンドウシステム関連の誤動作の温床となると思います。現代においてウィンドウシステムの不具合は操作するUIの土台の信頼の問題につながりますから、あまり嬉しい状況とはいえません。
ちなみにWindowsはこのデバイスドライバに関する問題に対して、マイクロソフトからの署名の入ったドライバでないと普通のユーザーはインストールすらできないようにしていたりします。これは過去にドライバ関連での不具合でカーネルを巻き込んでクラッシュするという事例がいくつか発生していたからのようです。この話はデバイスドライバの不具合は想定よりも問題が大きくなりやすいことを示唆しています。
バッファ
特にストレージなどのブロックデバイスに関しては、バッファと呼ばれる概念が導入されています。このバッファというのはファイルにおけるページキャッシュよりもさらに粒度の細かいソフトウェア実装のキャッシュを指します。これによりデバイスドライバの呼出の時間をより短くできる可能性があります。ただしページキャッシュという上位の存在もあるので、ここがどれくらいの貢献をするかはカーネルメンテナーやデバイスドライバの開発者ではない私は残念ながらまったくわからない状況です。
Unixにおけるファイル権限とユーザー権限による任意アクセス制御 DAC: Discretionary Access Control
UNIXはもともとひとつのコンピュータを複数人で利用するという要件に基づいて作られたカーネルです。このため、ファイルを誰がつくったか、誰が読み書き実行してもいいか、というルール、すなわちアクセス制御がかなり昔から設計されています。これがファイル権限とユーザー権限という形で現代のUnixとなるLinuxやMacなどについても実装されています。すべてがファイルであるが故に、アクセス制御の実装方法はファイルシステムと連動した実装がされているとみなすことができるので、直感的でわかりやすいです。
これは任意アクセス制御、DAC: Discretionary Access Controlと呼ばれる枠組みに含まれます。
ファイル権限に関しては先ほども実例として見せた以下のような情報において、rwxの並びごとに、ファイル所有者、所有者のグループ、それ以外のユーザーという順で設定できます。ファイルの所有者以外は見えないようにするなら、所有者のグループやそれ以外のユーザーからは見えなくしてしまう、といったことができるというわけです。
% ls -la
drwxr-xr-x@ 5 kurawo staff 160 3 12 08:08 .
drwxr-xr-x 11 kurawo staff 352 1 9 13:09 ..
-rw-r--r-- 1 kurawo staff 3496 3 12 08:11 unixfile.txt
このアクセス制御のアイデアはすさまじく、現代のネットワーク上のコンピュータという人類史上最もコンピュータの脆弱性を抱えやすい状況下においても奪ったユーザーの権限が低かったり、ただ単にブラウザ上のjsコードを実行する程度ではコンピュータをジャックすることも、情報を読み出すことすらもできなくなっています。このアクセス制御のアイデアは、WindowsもNT系になるときに取り入れられています。
例えば一般ユーザーの状態でただ単にjsのコードを通常ユーザーでブラウザで実行されていても危険な操作が成立しなかったりします。jsにおいてはWebページ経由での攻撃を防ぐために、ファイルの内容を直接書き換えたりネットワーク接続の解説をしたりはできないように設計されているから、という部分は多いにあるのですが。
任意アクセス制御(DAC)だけでは現代の攻撃を防ぎきれない
ただし、これらは一般ユーザーは、という制約がついており、常に新しいソフトウェアをインストールしているような現代においては通じない部分も生まれています。ソフトウェアのインストールにおいては、結局UnixもWindowsも管理者権限が必要になってしまうのです。これが結果的にある日届いた郵便物型爆弾を開けて爆発させてしまうようなことにもつながります。
具体的にはメールの中に入ったマクロまみれのOffice系ファイルやbatファイルが盗聴やファイル暗号化をする実行ファイルを管理者権限で自動インストールしてしまったりなどです。Windows特有の問題にも見えますが、これがMacやUbuntuに置き換わってもシェルスクリプトなどに置き変わるだけで状況はそう変わりないでしょう。メールで送受信していいのはpdfやjpegやtxtだけ、OfficeやAdobe系ファイルのやりとりは合意の上でストレージ上で、とかをすべての会社が運用ルールを定めない限りはメールによる悲劇は終わりがみえません。
これらは人間のチェック機能を乗り越えて実行させようとするデザインがなされており、現代人であっても、Microsoft Defender(元Windows Defender)であっても、マルウェアかどうか判別できず攻撃が実行されてしまうとみなしておかなければなりません。
またクライアントと比較してインストールするものを最小にするサーバーや整備された開発用のUnixの場合、たとえメールやブラウザを開くことがなかったとしても、npmでインストールした一部のパッケージがクラッカーの手で汚染されていた、などのことが実際に起きています。yumやaptやdockerコンテナでも同様のことが起こりえるので、ちゃんと動作などの検証をしてから本番環境に送り込む、などをしなければ避けるのは難しいでしょう。
これらの脆弱性の根本的な理由は、管理者および管理者の実行したソフトウェアはすべてのリソースの読み書きを実行可能となる、というところから来ています。つまり自分の足を自分で撃ち抜くということまでは防げないのです。
任意アクセス制御(DAC)を補完する強制アクセス制御 MAC: Mandatory Access Control
そこでここ最近はこのDAC(任意アクセス制御)の制御だけでないソフトが運用されています。RHEL系におけるSELinuxやUbuntuにおけるAppArmorなどの、強制アクセス制御 MAC: Mandatory Access Controlです。Windowsの場合ならアクセス制御リストが類似品となると思います。
この強制アクセス制御では、アプリケーション、よりわかりやすく言えばアプリケーションが生成するプロセスごとに、どのファイルにアクセスをしていいかを厳密に定義可能にするものとなります。つまり、ユーザーの起動したプロセスが必要以上のファイルの書き換えをできなくしたり、ネットワークアクセスにも制限をかけたり、種類によってはシステムコールの制御すらもできます。これにより自動実行されるという被害を限定的にすることが可能となります。
つまり、クライアントでメールやサイトを開けて危険なコードをダウンロード・実行してしまう類を防げるかどうかは微妙ですが、サーバーの自動実行されるアプリにおいてはこの内容を定義することで、ユーザーのふりをしたクラッカーやそのボットたちによる攻撃を防ぐ手立てとなるはずです。
ただしあくまで利用されないリソース、例えばファイルなどのみで、利用されなければならないDBデータのファイルの保護ができるわけではないので、アプリのフレームワークやDBの管理方法などについては別途ハードニングを重ねる必要があります。
なおこの強制アクセス制御の実装となるSELinuxやAppArmorはデフォルトでオンになっていて、一般的なWebアプリケーションを動かそうと思ったりDockerを利用した程度では邪魔をしてくることはありません。なので即座にSELinuxを切ったりはあまりおすすめはできないです。
もしもMACによって無効化されたりなどが発生した場合は詳細な理由を追って要件を見直したりMACの設定をカスタマイズするのが妥当だと思われます。
おわりに
Unixファイルシステム、ブロックI/Oデバイス、権限の関係性と通してUnixはすべてがファイルはどう実現されているかを見たうえでアクセス制御の内容にまで踏み込んだ内容となりました。私としてもなかなかとっつきづらい内容でぼんやりとしか覚えてないことがほとんどだったのでこの機会に整理ができてよかったと思っています。この記事が皆様のUnixの理解のきっかけとなっていただけたなら幸いです。
参考文献・サイト
Robert Love「Linuxシステムプログラミング」
https://www.amazon.co.jp/dp/4873113628/ref=cm_sw_r_tw_dp_89538ATF8A6H2KC27PY7
山森 丈範「Linuxシステムコール基本リファレンス」
https://www.amazon.co.jp/dp/4774195553/ref=cm_sw_r_tw_dp_QKW83MSPP8SCA7AB7AXF
青柳 隆宏「はじめてのOSコードリーディング」
https://www.amazon.co.jp/dp/B0821XY1QJ/ref=cm_sw_r_tw_dp_CFM2JG589SGXY2JFPZ43
えんでぃの技術ブログ「Linuxプロセスアクセス制御の概要」
https://endy-tech.hatenablog.jp/entry/linux_processes_access_control_basics