4 ポイント 投稿者 GN⁺ 2023-08-31 | 1件のコメント | WhatsAppで共有
  • FomosはRustで構築された実験用OSであり、Non-Unix OSのアイデアとexo-kernelパターンの課題を理解するためのプロジェクト
  • グラフィック出力、動的割り当て、同時アプリロードと実行、Virtioマウス・キーボード対応、協調スケジューリングを提供
  • アプリは pub extern "C" fn _start(ctx: &mut Context) -> i32 形式の単一関数として扱われ、標準ライブラリなしでOS機能を Context を通じて受け取る構造
  • Context はログ、PID、フレームバッファ、calloccdallocstore、入力などの機能と状態を保持する構造体であり、新しい機能を末尾に追加して古いアプリとの互換性を維持する方式
  • システムコールはなく、アプリは return でOSに制御を戻し、その後 start 関数が再び呼び出される協調実行モデル
  • アプリの状態は Context.store に保存でき、カーネルループはアプリ一覧を巡回し、各アプリの _start(Context::new(...)) を呼び出す単純化された構造
  • すべての機能と副作用が Context を通じて渡されるため、Context の関数置き換えやラッピングによってサンドボックス化、計測、デバッグを構成できる前提
  • 現時点でセキュリティは実装されておらず、アプリが他のアプリのRAMを確認できる状態であり、コンテキストスイッチとアプリごとの仮想メモリスタックなしでデータセキュリティを実装する計画がある
  • 未実装の項目は永続ストレージ、GPU対応、ネットワーキング、アプリ間でのデータと機能共有のための抽象化であり、Virglは作業中
  • ビルドは ./build.sh で実行し、rust nightlygcc、VirglおよびSDLフラグ付きのqemuが必要になる場合がある

1件のコメント

 
GN⁺ 2023-08-31
Hacker News のコメント
  • 気に入らない点は、プリエンプティブなシステムでは while (true) がシステムを遅くすることはあっても、協調型システムでは制御を返さない瞬間にマシンが事実上停止するという点
    セキュリティの観点でも、こういうシステムはサービス拒否攻撃があまりに簡単になり、あるアプリのバグ 1 つがシステム全体に波及し得る
    OS 開発者ではないので、間違っていたら訂正してほしい

    • その通りで、その反論は論点ずらしに近いと思う
      OS が協調的マルチタスクを捨てた理由は、あらゆる「暴走するリソース使用」の問題を永遠に解決したからではなく、UI スレッドのブロックや偶発的な無限ループのような単純なアプリレベルのエラーが、システム全体の状態を壊してしまうから
      任意のプログラムを実行するよう設計されたシステムでは、かなり致命的
    • 全か無かで見るべきではない
      アプリに協調的なスケジューリングを許しつつ、while(true){} がシステムを無限に占有できないようにすることはできる
      たとえばアプリごとに制限時間を設けたり、もっと実験的にはループを検出して余裕のある制限時間を置いたりできる
      プログラマーにとっては、無関係なプログラム間の隔離が最大で、関連するプログラム間の隔離が最小のときに扱いやすいが、ユーザーにとっては、計算資源は強く隔離され、ストレージや権限のようなものはあまり遮られていないときに使いやすい
      実際には協調型よりプリエンプティブ型に近いが、少し協調型でもある複雑なスケジューリングが必要になる
      Linux でもフォーク爆弾のような悪性プログラムは、特にスワップが有効だとシステムを停止させるのは難しくなく、プリエンプティブスケジューラがあっても、1 つのプログラムがシステムスレッドの 99% を占めれば、事実上ほとんどそのプログラムが実行される
    • 作者です
      スケジューリングはスペクトラムであり、現在の OS もプリエンプティブではあるが、ある程度は協調型でもある
      アプリは制御権を譲るかどうかを決められる
      逆に OS は協調型のままにしておき、リソース使用量のしきい値やタイマー割り込みが発生したときだけ、まれに失敗モードとしてコンテキストスイッチしてプリエンプティブ型に変わり、アプリを終了したあと協調型に戻ることもできる
      これは楽観的協調型、悲観的プリエンプティブ型と呼べるかもしれない
    • OS 開発者ではないが、コア数が違いを生むと思う
      while(true) ループはシングルコアシステムを崩壊させるが、マルチコアシステムでは必ずしもそうではない
      いま使っている OS の基本構造が作られた時代と現在では、トレードオフが変わっている可能性がある
    • Windows 3.1 の協調的マルチタスクの実験のあと、世界がプリエンプティブマルチタスクへ進んだのには理由がある
  • 特に「Fomos ではアプリはただの関数にすぎない」という部分が良かった
    Unix や Windows の実行ファイルは独立した関数に比べて非常に複雑なので、このように書かれたカーネルがどれほど素晴らしいものになるか想像するのも難しい
    Smalltalk/Squeak もこういう方式なのか気になるし、作者にはファイルシステム、タスクマネージャー、安全なメモリスタック、リソース共有まで続けてほしい
    もちろん最小限の概念実証要件として DOOM の実行も必要

    • Smalltalk では独立した関数というよりクラスのメソッドに相当するが、イメージ内のすべてのオブジェクトが互いにメッセージを送り合う、つまり互いのメソッドを呼び出すという点ではその通り
      Lisp マシンの OS のほうが近く、初期にはオブジェクトシステムなしで独立した関数同士が呼び出し合い、後には引数のクラスに特化したジェネリック関数になった
    • 「アプリはただの関数」という言葉は、グリーンフィールド開発の呪いのように聞こえる
      設計者たちが、ほかの OS がなぜ今では欠かせないものを必要とするようになったのか、まだ見つけていない状態に見える
    • 実際のところ、アプリが int main() { … } 関数であるほかの OS と何が違うのか気になる
    • これはユニカーネルの定義に合っているように見える
      Rust 製の別の例として https://github.com/hermit-os/hermit-rs がある
    • 古い Classic MacOS を動かしてみればいい
  • アイデアは悪くないが、古い項目と互換性を保つために 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は素晴らしいはずだが、このモデルはそうしたものを排除しているようで、十分な性能を出すのが難しいかもしれない
    非同期をサポートしないのも奇妙で、自然な中断点につなげられるから
    ただしそうするには、アプリケーションの状態をディスクに明示的に保存し、読み戻すという設計をかなり諦める必要がありそうで、コストは大きく見える
    ネットワーキングも似た理由で、少なくとも効率的に行うのは難しくなる可能性があると思う

    • 推測だが、非同期 I/O は Context に I/O リクエストを更新して戻り、準備ができたら関数が再度呼び出される形で実装されるのだと思う
      この関数は任意の状態を引数として受け取るイベントループの末端のように見えるので、イベントループが行うことは概ね一般化できそう
      ただし、言語レベルのコルーチンと非同期サポートは諦めることになる
  • 挙げられている例はあまりにも作為的
    プリエンプティブなオペレーティングシステムでは、アプリは通常、スレッドのデッドロックや無限ループのように、全体を協調型にしない形で停止する
    またプリエンプティブなシステムなら、アプリがスレッドやファイルを作りすぎたり、メモリを使いすぎたりした場合、実質的に協調型になるよりずっと前に終了させられる
    私たちのシステムは単により寛容なだけ
    そのうえ「サンドボックス化は前提さえ受け入れれば無料」と言いながら、「どのアプリでも他のアプリのRAMを簡単に確認でき、これは解決が難しい問題」とも言っているので、サンドボックス化は無料ではない
    それでも面白いアイデアだし、作者がうまくいくことを願っている

    • ブラウザの世界では、開いているすべてのサイトがブラウザのヒープメモリを共有しているが、互いに干渉しない
      この問題の解法は、関数、つまりアプリケーションを包むクロージャを作り、アプリ自身のContextのように動作させることかもしれない
      アプリがアプリを開けるなら、あるいはアプリが別のアプリにとってのオペレーティングシステムになれるならどうだろう、と思う
    • その例は、すべてのシステムがWindowsやLinuxのようにサンドボックス化がひどいと仮定しているので、さらに作為的
      堅牢なサンドボックス化のためにきちんと設計されたシステムなら、すべてのリソースに制限を設け、制限に達したらリクエストを拒否する
  • Fomosがプロセス実行ファイルをどう区別しているのか気になる
    Linuxにおけるプロセスは、argv/envpポインタ、スタック、ヒープ、シグナルマスク、ファイルハンドルテーブル、シグナルハンドラ、実行可能メモリを含む仮想アドレス空間と、uid、gidのようなカーネル内部データである
    実行ファイルは、execveシステムコール時にローダがそのアドレス空間を埋めるだけのビットを含むファイルである
    実行ファイルがなくてもclone3forkでプロセスは作れるし、カーネルはELFを使い、ユーザー空間の大半はGLIBCのRTLDローダを使うが、特定の実行ファイル形式でプロセスを作るために必ずしもその両方が必要なわけではない
    位置独立コードのない静的リンク実行ファイルは、アセンブラの観点では「ただの関数」に近いが、ASLRなしでランタイムシンボルを解決すると、依存関数のアドレスが分かったときにバッファオーバーフロー攻撃に弱くなる
    glibcの欠点とPOSIXプロセスモデルの代替は欲しいが、Unix実行ファイルの複雑さのかなりの部分は本質的なものだと思う
    ランタイムシンボル解決は難しいが有用で、任意のインタプリタを許すのは厄介でもLinuxがWindowsやMacOSより強い部分であり、安定したシステムコールでカーネルインターフェースを提供することがLinuxの強みである

    • もう少し考えてみると、大きな誤りの一つは実行ファイル形式の同質化のように思える
      MacはMach-O、WindowsはPE、LinuxはELFという具合だが、実行/リンク形式の多様なエコシステムを持てない理由はない
      コードをロードするモデルが非常に単純なオペレーティングシステムは、そうした実験に適した場所である
    • Linuxの強みは安定したABIというよりドライバだと思っていた
      安定したABIがLinuxだけに特有の点というわけでもなく、その判断の有用性もかなり疑わしいが、ドライバサポートは素晴らしく否定しにくい
    • Zircon(Fuchsia)がこれをどう扱っているのか見たことがあるのか気になる
      かなり興味深い
  • 信頼できない協調型アプリで、どの程度のセキュリティと安全性をどう実現できるのか分からない
    どのアプリでもCPUを無期限に握り続け、カーネルと他のアプリを停止させられる
    私たちがプリエンプティブスケジューリングのオペレーティングシステムを使う理由は、誤動作するアプリを残りのシステムを壊さずに止められるからである

    • 2000年代半ばにMicrosoft Researchに、.NETだけで書かれたプロトタイプのオペレーティングシステムがあったと記憶している
      プリエンプティブなマルチタスクは使っていたが、メモリ保護は強制せず、その代わりコンパイラをシステムサービスとして置き、システムコンパイラが作成して署名した実行ファイルだけを実行できるようにしていたと思う
      コンパイラがビルド時点でメモリ保護を保証するため、システムコールとプロセス間通信が非常に安価になった
      もう少し洗練されたコンパイラなら、必要な箇所にyield呼び出しを挿入して協調型マルチタスクも同様に強制できる
      一般的な停止問題は解けないが、静的解析によって終了する、または譲ることを証明できるプログラムの種類は依然として存在する
      証明できないプログラムだけを別扱いすればよく、ウォッチドッグタイマーで誤動作するプログラムを自動停止することもできる
    • SqueakやPharoのようなSmalltalkシステムでは、スレッドが止まるとユーザーがキーボードショートカットで実行を中断する
      信頼できないコードは「メイン」イメージでは実行せず、捨てられるVMで動かす
      ここでもハイパーバイザを使えば同じモデルを適用できるだろうが、Smalltalkシステムだけを単独で使う人はいないし、ある程度のインフラが必要である
  • 完全な再設計なしに、事実上、既存のOSがすでにやってきたことを繰り返さずに、このOSでセキュリティを実装できるのか気になる
    同じハードウェア上で動くアプリケーションのセキュリティを強制する方法は2つ知っている
    1つはランタイムで仮想メモリによってプロセスを分離する方法で、もう1つはロード時にローダーがコードによる任意メモリアクセスの有無を検証する方法
    後者は通常、JVMやSmalltalkのようにポインタ演算のない制限された命令セットのバイトコードだけを許可する仮想マシンで強制する
    Fomosの作者はコンテキストスイッチやメモリ分離などを望んでおらず、Rustコンパイラはバイトコードを生成しないが、ほかに方法はあるのだろうか?

    • Theseusは、バイトコードなしでRustで実装した2つ目の方式の例
      私の理解では、認証済みコンパイラでunsafe禁止のようなルールを強制するため、ここではソースコードが事実上バイトコードに相当する
      一見するとMidoriにかなり似ているが、細部の実装はかなり違う
      Theseusでは、ドライバやアプリケーションなどがELFオブジェクトで、すべてが1つの実行ファイル、つまりカーネルへ動的リンクされ、ホットアップグレードのような興味深い手法もある
      https://github.com/theseus-os/Theseus
      https://www.theseus-os.com/
    • 推測だが、すべての「プログラム」が1つのアドレス空間を共有しつつ、仮想メモリで特定の時点にアクセス可能なページの可視性を制限できるのかもしれない
      ランタイムでセグメンテーションフォルトが起きたら、呼び出し側がそのページへアクセスして呼び出す権限を持っているか、セキュリティトークンのようなものを確認する、という具合
      実際にどれほど実用的かは分からない
    • https://en.wikipedia.org/wiki/Capability-based_addressing ?
  • 協調的マルチタスクであっても、今はコアが非常に多いという点でClassic Mac OSの時代と同じではないと思う
    yieldしないプロセスが1つや2つあっても、必ずしもシステム全体を捕まえてしまうとは限らない
    関数が誤動作して戻らないなら、システムがコアを使い切った時点で終了させることもできるだろう
    協調的マルチタスクが必ずしも低性能を意味するわけではない
    タイムシェアリングはもともと巨大な単一CPUを複数ユーザーに分け与えるための方法だったが、今では単一ユーザーのマルチコアCPUが一般的なので、コアを使う別の方法を考える時期はとっくに来ている
    このプロジェクトが存在していること自体、本当に期待できる

    • 付け加えると、「協調的マルチタスクが必ずしも低性能を意味するわけではない」というのは、対話性能の悪さを意味したつもりだった
      このモデルではコンテキストスイッチがないので、むしろ性能が良くなる可能性がある
      なのでLinuxのタイムスライスを10秒のようなばかげた値に引き上げたら何が起きるのか気になってくる
  • セキュリティ計画について、もっと詳しく聞きたい
    全体として、こうした実験はOSがグリーンフィールド設計で改善され得ることを示していると思う
    Mirage OSを少し思い出す: https://mirage.io/