Fomos: Rustで構築された実験用オペレーティングシステム
(github.com/Ruddle)- FomosはRustで構築された実験用OSであり、Non-Unix OSのアイデアとexo-kernelパターンの課題を理解するためのプロジェクト
- グラフィック出力、動的割り当て、同時アプリロードと実行、Virtioマウス・キーボード対応、協調スケジューリングを提供
- アプリは
pub extern "C" fn _start(ctx: &mut Context) -> i32形式の単一関数として扱われ、標準ライブラリなしでOS機能をContextを通じて受け取る構造 Contextはログ、PID、フレームバッファ、calloc、cdalloc、store、入力などの機能と状態を保持する構造体であり、新しい機能を末尾に追加して古いアプリとの互換性を維持する方式- システムコールはなく、アプリは
returnでOSに制御を戻し、その後start関数が再び呼び出される協調実行モデル - アプリの状態は
Context.storeに保存でき、カーネルループはアプリ一覧を巡回し、各アプリの_start(Context::new(...))を呼び出す単純化された構造 - すべての機能と副作用が
Contextを通じて渡されるため、Contextの関数置き換えやラッピングによってサンドボックス化、計測、デバッグを構成できる前提 - 現時点でセキュリティは実装されておらず、アプリが他のアプリのRAMを確認できる状態であり、コンテキストスイッチとアプリごとの仮想メモリスタックなしでデータセキュリティを実装する計画がある
- 未実装の項目は永続ストレージ、GPU対応、ネットワーキング、アプリ間でのデータと機能共有のための抽象化であり、Virglは作業中
- ビルドは
./build.shで実行し、rust nightly、gcc、VirglおよびSDLフラグ付きのqemuが必要になる場合がある
1件のコメント
Hacker News のコメント
気に入らない点は、プリエンプティブなシステムでは
while (true)がシステムを遅くすることはあっても、協調型システムでは制御を返さない瞬間にマシンが事実上停止するという点セキュリティの観点でも、こういうシステムはサービス拒否攻撃があまりに簡単になり、あるアプリのバグ 1 つがシステム全体に波及し得る
OS 開発者ではないので、間違っていたら訂正してほしい
OS が協調的マルチタスクを捨てた理由は、あらゆる「暴走するリソース使用」の問題を永遠に解決したからではなく、UI スレッドのブロックや偶発的な無限ループのような単純なアプリレベルのエラーが、システム全体の状態を壊してしまうから
任意のプログラムを実行するよう設計されたシステムでは、かなり致命的
アプリに協調的なスケジューリングを許しつつ、
while(true){}がシステムを無限に占有できないようにすることはできるたとえばアプリごとに制限時間を設けたり、もっと実験的にはループを検出して余裕のある制限時間を置いたりできる
プログラマーにとっては、無関係なプログラム間の隔離が最大で、関連するプログラム間の隔離が最小のときに扱いやすいが、ユーザーにとっては、計算資源は強く隔離され、ストレージや権限のようなものはあまり遮られていないときに使いやすい
実際には協調型よりプリエンプティブ型に近いが、少し協調型でもある複雑なスケジューリングが必要になる
Linux でもフォーク爆弾のような悪性プログラムは、特にスワップが有効だとシステムを停止させるのは難しくなく、プリエンプティブスケジューラがあっても、1 つのプログラムがシステムスレッドの 99% を占めれば、事実上ほとんどそのプログラムが実行される
スケジューリングはスペクトラムであり、現在の OS もプリエンプティブではあるが、ある程度は協調型でもある
アプリは制御権を譲るかどうかを決められる
逆に OS は協調型のままにしておき、リソース使用量のしきい値やタイマー割り込みが発生したときだけ、まれに失敗モードとしてコンテキストスイッチしてプリエンプティブ型に変わり、アプリを終了したあと協調型に戻ることもできる
これは楽観的協調型、悲観的プリエンプティブ型と呼べるかもしれない
while(true)ループはシングルコアシステムを崩壊させるが、マルチコアシステムでは必ずしもそうではないいま使っている OS の基本構造が作られた時代と現在では、トレードオフが変わっている可能性がある
特に「Fomos ではアプリはただの関数にすぎない」という部分が良かった
Unix や Windows の実行ファイルは独立した関数に比べて非常に複雑なので、このように書かれたカーネルがどれほど素晴らしいものになるか想像するのも難しい
Smalltalk/Squeak もこういう方式なのか気になるし、作者にはファイルシステム、タスクマネージャー、安全なメモリスタック、リソース共有まで続けてほしい
もちろん最小限の概念実証要件として DOOM の実行も必要
Lisp マシンの OS のほうが近く、初期にはオブジェクトシステムなしで独立した関数同士が呼び出し合い、後には引数のクラスに特化したジェネリック関数になった
設計者たちが、ほかの OS がなぜ今では欠かせないものを必要とするようになったのか、まだ見つけていない状態に見える
int main() { … }関数であるほかの OS と何が違うのか気になるRust 製の別の例として https://github.com/hermit-os/hermit-rs がある
アイデアは悪くないが、古い項目と互換性を保つために Context 構造体へ新しい関数を足し続ける方式は、後方互換性地獄への道
古くなったり廃止されたりした項目を Context 構造体から取り除けないよう、自分で自分を閉じ込めることになる
より良い方法は、OS とアプリの間にセマンティックバージョニングを導入することだと思う
アプリがどの OS バージョン向けにビルドされた、または依存しているかを宣言すれば、OS は互換性を確認し、それに合った Context 構造体のバージョンを渡せる
後方互換性の問題の大半は残るが、カーネル内にメジャー/マイナーバージョン別の構造体を複数置く方式で、Context 構造体をきれいに保てる
良いアイデアだが、ランタイムインターフェースが 1 つだけという単純さも気に入っている
いずれにせよ OS がすべてのバージョンを処理しなければならないなら、将来のアプリにはパディングを使って「きれい」に感じられるようにすることもできる
struct Context{ padding: [u8;256], // old stuff ctx: ContextV42 }ただ、こう書いてみると少し間違っている感じもする
アプリが自分のバージョンを宣言するのは、ELF のような実行ファイル形式がすでに解決している問題のように感じるので、別案を試している
「どうやって sleep したり非同期に待ったりするのか? ただ return すればいい」という部分は少し奇妙
io_uring式の非同期 I/Oは素晴らしいはずだが、このモデルはそうしたものを排除しているようで、十分な性能を出すのが難しいかもしれない非同期をサポートしないのも奇妙で、自然な中断点につなげられるから
ただしそうするには、アプリケーションの状態をディスクに明示的に保存し、読み戻すという設計をかなり諦める必要がありそうで、コストは大きく見える
ネットワーキングも似た理由で、少なくとも効率的に行うのは難しくなる可能性があると思う
この関数は任意の状態を引数として受け取るイベントループの末端のように見えるので、イベントループが行うことは概ね一般化できそう
ただし、言語レベルのコルーチンと非同期サポートは諦めることになる
挙げられている例はあまりにも作為的
プリエンプティブなオペレーティングシステムでは、アプリは通常、スレッドのデッドロックや無限ループのように、全体を協調型にしない形で停止する
またプリエンプティブなシステムなら、アプリがスレッドやファイルを作りすぎたり、メモリを使いすぎたりした場合、実質的に協調型になるよりずっと前に終了させられる
私たちのシステムは単により寛容なだけ
そのうえ「サンドボックス化は前提さえ受け入れれば無料」と言いながら、「どのアプリでも他のアプリのRAMを簡単に確認でき、これは解決が難しい問題」とも言っているので、サンドボックス化は無料ではない
それでも面白いアイデアだし、作者がうまくいくことを願っている
この問題の解法は、関数、つまりアプリケーションを包むクロージャを作り、アプリ自身のContextのように動作させることかもしれない
アプリがアプリを開けるなら、あるいはアプリが別のアプリにとってのオペレーティングシステムになれるならどうだろう、と思う
堅牢なサンドボックス化のためにきちんと設計されたシステムなら、すべてのリソースに制限を設け、制限に達したらリクエストを拒否する
Fomosがプロセスと実行ファイルをどう区別しているのか気になる
Linuxにおけるプロセスは、argv/envpポインタ、スタック、ヒープ、シグナルマスク、ファイルハンドルテーブル、シグナルハンドラ、実行可能メモリを含む仮想アドレス空間と、uid、gidのようなカーネル内部データである
実行ファイルは、
execveシステムコール時にローダがそのアドレス空間を埋めるだけのビットを含むファイルである実行ファイルがなくても
clone3やforkでプロセスは作れるし、カーネルはELFを使い、ユーザー空間の大半はGLIBCのRTLDローダを使うが、特定の実行ファイル形式でプロセスを作るために必ずしもその両方が必要なわけではない位置独立コードのない静的リンク実行ファイルは、アセンブラの観点では「ただの関数」に近いが、ASLRなしでランタイムシンボルを解決すると、依存関数のアドレスが分かったときにバッファオーバーフロー攻撃に弱くなる
glibcの欠点とPOSIXプロセスモデルの代替は欲しいが、Unix実行ファイルの複雑さのかなりの部分は本質的なものだと思う
ランタイムシンボル解決は難しいが有用で、任意のインタプリタを許すのは厄介でもLinuxがWindowsやMacOSより強い部分であり、安定したシステムコールでカーネルインターフェースを提供することがLinuxの強みである
MacはMach-O、WindowsはPE、LinuxはELFという具合だが、実行/リンク形式の多様なエコシステムを持てない理由はない
コードをロードするモデルが非常に単純なオペレーティングシステムは、そうした実験に適した場所である
安定したABIがLinuxだけに特有の点というわけでもなく、その判断の有用性もかなり疑わしいが、ドライバサポートは素晴らしく否定しにくい
かなり興味深い
信頼できない協調型アプリで、どの程度のセキュリティと安全性をどう実現できるのか分からない
どのアプリでもCPUを無期限に握り続け、カーネルと他のアプリを停止させられる
私たちがプリエンプティブスケジューリングのオペレーティングシステムを使う理由は、誤動作するアプリを残りのシステムを壊さずに止められるからである
プリエンプティブなマルチタスクは使っていたが、メモリ保護は強制せず、その代わりコンパイラをシステムサービスとして置き、システムコンパイラが作成して署名した実行ファイルだけを実行できるようにしていたと思う
コンパイラがビルド時点でメモリ保護を保証するため、システムコールとプロセス間通信が非常に安価になった
もう少し洗練されたコンパイラなら、必要な箇所に
yield呼び出しを挿入して協調型マルチタスクも同様に強制できる一般的な停止問題は解けないが、静的解析によって終了する、または譲ることを証明できるプログラムの種類は依然として存在する
証明できないプログラムだけを別扱いすればよく、ウォッチドッグタイマーで誤動作するプログラムを自動停止することもできる
信頼できないコードは「メイン」イメージでは実行せず、捨てられるVMで動かす
ここでもハイパーバイザを使えば同じモデルを適用できるだろうが、Smalltalkシステムだけを単独で使う人はいないし、ある程度のインフラが必要である
完全な再設計なしに、事実上、既存のOSがすでにやってきたことを繰り返さずに、このOSでセキュリティを実装できるのか気になる
同じハードウェア上で動くアプリケーションのセキュリティを強制する方法は2つ知っている
1つはランタイムで仮想メモリによってプロセスを分離する方法で、もう1つはロード時にローダーがコードによる任意メモリアクセスの有無を検証する方法
後者は通常、JVMやSmalltalkのようにポインタ演算のない制限された命令セットのバイトコードだけを許可する仮想マシンで強制する
Fomosの作者はコンテキストスイッチやメモリ分離などを望んでおらず、Rustコンパイラはバイトコードを生成しないが、ほかに方法はあるのだろうか?
私の理解では、認証済みコンパイラで
unsafe禁止のようなルールを強制するため、ここではソースコードが事実上バイトコードに相当する一見するとMidoriにかなり似ているが、細部の実装はかなり違う
Theseusでは、ドライバやアプリケーションなどがELFオブジェクトで、すべてが1つの実行ファイル、つまりカーネルへ動的リンクされ、ホットアップグレードのような興味深い手法もある
https://github.com/theseus-os/Theseus
https://www.theseus-os.com/
ランタイムでセグメンテーションフォルトが起きたら、呼び出し側がそのページへアクセスして呼び出す権限を持っているか、セキュリティトークンのようなものを確認する、という具合
実際にどれほど実用的かは分からない
協調的マルチタスクであっても、今はコアが非常に多いという点でClassic Mac OSの時代と同じではないと思う
yieldしないプロセスが1つや2つあっても、必ずしもシステム全体を捕まえてしまうとは限らない
関数が誤動作して戻らないなら、システムがコアを使い切った時点で終了させることもできるだろう
協調的マルチタスクが必ずしも低性能を意味するわけではない
タイムシェアリングはもともと巨大な単一CPUを複数ユーザーに分け与えるための方法だったが、今では単一ユーザーのマルチコアCPUが一般的なので、コアを使う別の方法を考える時期はとっくに来ている
このプロジェクトが存在していること自体、本当に期待できる
このモデルではコンテキストスイッチがないので、むしろ性能が良くなる可能性がある
なのでLinuxのタイムスライスを10秒のようなばかげた値に引き上げたら何が起きるのか気になってくる
セキュリティ計画について、もっと詳しく聞きたい
全体として、こうした実験はOSがグリーンフィールド設計で改善され得ることを示していると思う
Mirage OSを少し思い出す: https://mirage.io/