🌌

Rust 製 OS、Redox OS での一括 FD 送信実装とファイルテーブル分割

に公開

はじめに

こんにちは、isan です。
5 月から参加している、 Rust 製 OS である Redox OS の Summer of Code のプロジェクトを無事完了しました。
今回は、その成果である UNIX ドメインソケット(UDS)実装、一括 FD 送信サポート、ファイルテーブル拡張について記事にさせていただきます。(この記事は公式ブログで公開されている RSoC 2025: Final Report: Unix Domain Sockets, Bulk FD Passing, and Separating File Tablesの日本語版です。)

この記事の目的

この記事では、私が Summer of Code で行った各プロジェクトについて解説し、それらが今後どのような機能につながるかについて解説します。それを通して、Rust OS とその今後の展開に興味を持ってもらうことを目的としています。
この記事では、次の流れで説明します。

  • Redox OS とは
  • UNIX ドメインソケット の改善
  • 一括 FD 送信機能 の実装
  • ファイルテーブル分割と "Upper" ファイルテーブルの導入
  • Redox OS の今後: Capability-based securityio_uring-like IO

Redox OS とは

Redox OS について簡単に解説します。
Redox OS はオープンソースの Rust 製 OS です。
Redox OS は主に次のような特徴を持ちます。(公式サイトより https://www.redox-os.org/)

  • マイクロカーネル設計(プロセスマネージャやドライバがユーザスペースで実行)
  • UNIX ライク
  • Rust 製の libc も開発(relibc)
  • GUI を持つ(Orbital)

ネットワーク機能や、gitやvim などの開発ソフトウェアが動くなど、ある程度成熟しています。

UNIX ドメインソケット

少しだけ、UNIX ドメインソケットと Redox OS 上での実装についておさらいしましょう。
UDS は、強力なプロセス間通信機能です。ソケットを使用する点でネットワークソケットと似ていますが、ネットワークスタックを経由する必要がないため、ローカル環境での通信が非常に速いという特徴があります。
Redox OS 上では、IPCD(Inter-Process Communication Daemon)が提供する uds スキームとして実装しました。

UNIX ドメインソケットの実装と FD 送信に関しては別で記事が用意されています。詳しく知りたい方は Rust 製 OS、 Redox OS に UNIX ドメインソケットを実装したをご覧ください。

UNIX ドメインソケットの改善

前回の記事以降 UNIX ドメインソケットに関しては、Redox のファイルシステムデーモンである RedoxFS との統合によるセキュリティ強化を行いました。(少しだけ以前の記事でも触れられています。)
以前の実装では UDS ソケットは各デーモンの役割を明確にするため IPCD 内でのみ作成され、uds スキームで管理されていました。しかし、この方法では RedoxFS が持つ豊富な権限チェック機能を利用できず、セキュリティ上の問題がありました。
Redox では、リソースに "Scheme-rooted Path" を介してアクセスされます。これは uds スキームでも例外ではありません。ソケットは /scheme/uds/ 配下に作成され、このパスを使用して bind または connect が行われます。このとき、uds スキームだけでは、そのパスにリソースを bind する、あるいはそのリソースに connect する権限があるかをチェックすることが困難になります。これは、セキュリティ上の問題を引き起こす可能性があります。
これに対処するために、UDS の bindconnect 操作の RedoxFS への統合を行いました。
現在は、プロセスが UDS の bind または connect 処理を行う際に、IPCD だけでなく、RedoxFS とも通信するようになり、ファイルシステム上のアクセスコントロールを利用したパーミッションチェックを行っています。

これは 今後 Capability-base セキュリティを導入した後変更されるかもしれませんが、現状とれる上で最善のアプローチだと思われます。

IPCD と RedoxFS を含んだ新しい bindconnect 操作を次に示します。

Bind 操作の流れ

  1. プロセス: bind 関数を呼び出します。
  2. bind: IPCD は Bind(SYS_CALL) を呼び出します。(これは主に Socket への名付けを目的とします。)
  3. IPCD: Bind(SYS_CALL) リクエストを受け取り、ソケットに名前をつけ、ソケットに紐づけられたトークンを発行します。
  4. bind: ソケットファイルを作成するディレクトリの FD を open システムコールを使用して開く。
  5. bind: ディレクトリ FD を使って、ソケットの FD を RedoxFS に送信する。
  6. RedoxFS: ソケットの FD を受け取り、ソケットのファイルをディレクトリ配下に作成し、ソケットの FD とマッピングします。
  7. プロセス: bind 結果を受け取ります。

もし、Bind(SYS_CALL) 以降の操作に失敗したら、bind 関数は Unbind(SYS_CALL) を呼び、IPCD 内のソケットの名前を破棄します。
図1: Bind 操作フロー

Connect 操作の流れ

  1. プロセス: connect 関数を呼び出します。
  2. connect: ソケットファイルを open システムコールを使用して開く。
  3. RedoxFS: open システムコールリクエストを受け取り、呼び出し元のプロセスがそのファイルにアクセスする権限があるかどうかをチェックし、FD を返却します。
  4. connect: 受け取った FD を介して Connect(SYS_CALL) 呼び出し、RedoxFS にリクエストを送信します。
  5. RedoxFS: Connect(SYS_CALL) をリクエストを受け取り、そのソケットファイルにマップされたソケット FD を介して、IPCD からソケットのトークンを取得するための GetToken(SYS_CALL) を呼び出します。
  6. IPCD: GetToken(SYS_CALL) リクエストを受け取り、ソケットのトークンを返却します。
  7. RedoxFS: 受け取ったトークンと返却します。
  8. connect: 接続元ソケットの FD と受け取ったトークンを使って Connect(SYS_CALL) を呼び出し、IPCD にリクエストを送信します。
  9. IPCD: Connect(SYS_CALL) リクエストを受け取り、トークンを検証後、トークンから接続先ソケットを逆引きし、コネクト処理を行います。
  10. プロセス: connect 結果を受け取ります。

図2: Connect 操作フロー

この統合により、UDS の bindconnect 操作は RedoxFS のパーミッションチェックを利用することができるようになり、正しい許可を受けたプロセスのみがソケットファイルの作成とソケットへの接続を行えることを保証できるようになりました。

UNIX ドメインソケットが何につながるのか?

UNIX ドメインソケットの実装は Redox OS の Wayland サポートトラッキングイシューの 1 つです。
Wayland は Linux や UNIX ライク OS の GUI プロトコルであり、UNIX ドメインソケットはそのコンポジタとクライアント間の通信に使用されています。

一括 FD 送信実装とファイルテーブル分割

UDS の実装後、私は一括 FD 送信実装とファイルテーブル分割を行いました。
一括 FD 送信は、その名の通りプロセスが複数の FD を一括で送信することを可能にします。
ファイルテーブル分割は、FD 番号空間を POSIX(lower) と "upper" 空間に分割するというもので、ユーザから"隠れた FD" を利用可能にします。

一括 FD 送信

現在 Redox OS では、FD 送信機能を sendfd システムコールと named dup メカニズムにより実現しています。
これは、sendfd システムコールで FD を送信し、"recvfd" という特殊な引数で dup を呼び出すことで、FD を受信するコードです。

// Sending process
let fd_to_send = open("/var/log/app.log", O_WRONLY | O_CREAT | O_APPEND);
syscall::sendfd(socket, fd_to_send);

// Receiving process
let received_fd = syscall::dup(socket, "recvfd");
// Now the receiving process can use `received_fd` to write to the log file.
syscall::write(received_fd, b"Log message from worker\n");
syscall::close(received_fd);

しかし、UDS の sendmsgrecvmsg のように複数のFDを送受信する場合、FDの数だけこの操作を繰り返す必要があり、非効率でパフォーマンスのボトルネックになります。
これに対処するため、SYS_CALL を使用し、複数の FD を一括で送信する機能を実装しました。
SYS_CALL Redox OS でスキームと複雑な通信を一回のシステムコールで行うことができる強力な システムコールですが、一括 FD 送信を扱えるほど柔軟です。
SYS_CALLCallFlags というフラグを引数に取りますが、これにカーネルが普通のデータと FD を区別するための CallFlags::FD という新しいフラグを追加しました。
それと同時に、データが書き込み限定あるいは読み込み限定を表す、一般的な CallFlags::WRITECallFlags::READ を追加しました。
これを組み合わせることで、リクエストが一括 FD 送信なのか受信なのかを判別することができるようになります。

bitflags! {
    pub struct CallFlags: usize {
        ~
        const WRITE = 1 << 9;
        const READ = 1 << 10;

        /// Indicates the request is a bulk fd passing request.
        const FD = 1 << 11;
        ~
    }
}

一括 FD 送信の流れを図 3 に示します。また、実装が気になる方はこちらをご覧ください

  1. 送信側プロセス: 送る FD のリストを作成します。
  2. 送信側プロセス: そのリストとともに CallFlags::FD | CallFlags::WRITE を指定して sys_call 関数 を呼び出します。
  3. カーネル: 送信側プロセスからリクエストを受け取り、フラグをチェックし、一括 FD 送信リクエストであると判断します。
  4. カーネル: リストにある FD を送信元プロセスのファイルテーブルから削除し、リクエスト ID をキーとしてのリクエスト状態管理インスタンスに挿入します。
  5. 同時に、FD の数を含むリクエストをスキームに送信します。
  6. スキーム: カーネルから FD 送信リクエストを受け取ります。
  7. スキーム: リクエストにある FD の数からバッファを作成し、そのリクエスト ID とともにObtainFd(SYS_CALL) を呼び出し、カーネルにリクエストを送信します。
  8. カーネル: スキームから ObtainFd(SYS_CALL) リクエストを受け取ると、リクエスト ID を使ってリクエスト状態管理インスタンスに挿入されている FD を取り出します。
  9. カーネル: 取り出した FD をスキームのファイルテーブルに追加します。同時に、追加された FD の番号をバッファに書き込みます。
  10. スキーム: 送信側プロセスにレスポンスを返却し、受信側プロセスからリクエストが届くまで FD を保持します。

  1. 受信側プロセス: 受け取る FD の番号を格納するためののバッファを作成します。
  2. 受信側プロセス: そのバッファとともに CallFlags::FD | CallFlags::READ を指定して sys_call 関数を呼び出します。
  3. カーネル: リクエストを受け取り、フラグをチェックし、一括 FD 受信リクエストであると判断します。
  4. カーネル: 空のベクタを、リクエスト ID をキーとしてリクエスト状態管理インスタンスに挿入します。これは、後でスキームからFDが返されたときに格納するための場所です。
  5. カーネル: 受信する FD 数を含んだリクエストをスキームに送信します。
  6. スキーム: 一括 FD 受信リクエストを受け取ります。
  7. スキーム: リクエストにあるバッファのサイズ分保持している FD を取り出してリストを作成し、そのリクエスト ID ととともに MoveFd(SYS_CALL) を呼び出し、カーネルにリクエストを送信します。
  8. カーネル: リクエストを受け取り、リストにある FD をスキームのファイルテーブルから削除します。
  9. カーネル: リクエスト ID を使ってリクエスト状態管理インスタンスに挿入されている空のベクタに FD を追加します。
  10. カーネル: FD を移動したことをレスポンスとしてスキームに返却します。
  11. カーネル: レスポンスを受け取り、リクエスト ID を使ってリクエスト状態インスタンスから FD を取り出します。
  12. カーネル: 取り出された FD を受信側プロセスのファイルテーブルに追加し、追加された FD 番号を受信リクエストのバッファに書き込みます。
  13. 受信側プロセス: 受信した FD の FD 番号を受け取ります。

図3: 一括 FD 送信フロー
これは、Rust で一括 FD 送受信を行うコード例です。call_wocall_ro はそれぞれ、CallFlags::WRITECallFlags::READ がデフォルトで設定された SYS_CALL のラッパ関数です。

// Send multiple FDs to another process using bulk FD passing.
// Assume `sender_sock` is a file descriptor to a socket,
// and `fds_to_send` is a vector of file descriptors.
let mut payload: Vec<u8> = Vec::with_capacity(fds_to_send.len() * mem::size_of::<usize>());
for &fd in fds_to_send {
    payload.extend_from_slice(&fd.to_ne_bytes());
}
libredox::call::call_wo(
    sender_sock,
    &payload,
    CallFlags::FD | flags,
    &[],
)?

// Receive multiple FDs from another process using bulk FD passing.
// Assume `receiver_sock` is a file descriptor to a socket,
// and `dst_fds` is a buffer to store the received file descriptors.
let dst_fds_bytes: &mut [u8] = unsafe {
    core::slice::from_raw_parts_mut(
        dst_fds.as_mut_ptr() as *mut u8,
        dst_fds.len() * mem::size_of::<usize>(),
    )
};
libredox::call::call_ro(
    receiver_sock,
    dst_fds_bytes,
    CallFlags::FD | flags,
    &[],
)?

また、追加で CallFlags::FD_CLONE というフラグも追加しています。これは現在の sendfd システムコールがデフォルトでムーブセンマンティクスであり、FD を保持するためには事前に dup を実行する必要があるという点を解決するためのものです。
このフラグを指定することで、FD を保持したまま送信することが可能です。

一括 FD 送信が何につながるのか?

一括 FD 送信により、一回で複数の FD を送信することが可能になり、システムコールを複数回呼び出すオーバーヘッドが解消されました。
これは、UDS のようなリソースの共有を行うアプリケーションで適応することが可能です。
現在 UDS の sendmsgrecvmsg はこの一括 FD 送信を使用していませんが、今後一括 FD 送信を使用するように再実装する予定です。

ファイルテーブル分割

ファイルテーブル分割はもう一つの重要な機能です。
現在のファイルテーブルは単一の空間に Redox が内部的に使用する目的の FD も含め、すべての FD が含まれています。
私は、このファイルテーブルを POSIX(lower)upper の二つの空間へ分割しました。
この分割はとてもシンプルで、現在コンテキストが保持しているファイルテーブルを、2 つのベクタをフィールドに持つ FdTbl という構造体に変更すれば良いだけです。
1 つめのファイルテーブルは、posix_fdtbl です。これは従来のファイルテーブルと同じものであり、POSIX の要求に従い、FD は最も小さい利用可能なインデックスに挿入されます。
2 つめのファイルテーブルは、upper_fdtbl です。これは、1 << (usize::BITS - 2) より上位のビットに予約された空間であり、また、POSIX の要求に従う必要がないため、FD を連続して挿入することも可能です。

#[derive(Clone, Debug, Default)]
pub struct FdTbl {
    pub posix_fdtbl: Vec<Option<FileDescriptor>>,
    pub upper_fdtbl: Vec<Option<FileDescriptor>>,
    active_count: usize,
}

pub const UPPER_FDTBL_TAG: usize = 1 << (usize::BITS - 2);

現在は、一括 FD 送信機能が Upper ファイルテーブル機能をサポートしており、CallFlags::FD_UPPER を指定することで、Upper ファイルテーブルに FD を連続して受け取ることもできます。

ファイルテーブル分割が何につながるのか?

現在は全く ファイルテーブル分割は利用されていません。(なんせ導入したばかりなので)
しかし、将来的に追加される多くの機能で利用される予定です。
例えば、Capability-based security を導入した際です。NAMESPACE FD や CWD FD、Redox が内部的に使用する目的で開かれた FD などをユーザプログラムから見えない、かつ POSIX の範囲と被らない FD として利用できるようにするために Upper ファイルテーブルを使用することができます。

Redox OS の今後: Capability-based security と io_uring-like IO

Redox は最近 NLnet から支援を受けたプロジェクトを開始しました。
NLnet は、Next Generation Internet(NGI) に運営されているプロジェクトの一つであり、インターネットに関する開発プロジェクトを支援するプログラムです。
NLnet に支援されたプロジェクトで有名なものだと、WireGuardTauri AppsServo、などでしょうか、また、NGI の他のプロジェクトで NixOS なども支援をしているようです。
Redox が支援を受け始めたプロジェクトは、"Capability-based security for Redox" と "io_uring-like IO for Redox" です。

  • Capability-based security: Redox OS 内の FD の表現を変更し、セキュリティとパフォーマンスを向上させながら、Capability-based security を導入する基盤を構築することを目的としたプロジェクトです。これに関連した RFC はこちらから確認できます。
  • io_uring-like IO: リングバッファを使ったコンポーネント間通信をサポートすることで、パフォーマンスの向上を目指すもので、ユーザ to ユーザ、ユーザ to カーネルそれぞれのリングバッファを構築するようです。(このプロジェクトについては、筆者もまだ理解を深めている最中です。)

私は、Capability-based security プロジェクトに参加しており、ネームスペースマネージャ in ユーザスペースの開発をおこなっています。
このプロジェクトは、現在カーネルにあるネームスペースマネージャ SchemeList をユーザスペースで動作するネームスペーススキームに再実装するというものです。
詳細は省きますが、open(path, ...) をネームスペーススキームを介して実行する openat(NAMESPACE_FD, path, ...) のラッパにし、ネームスペーススキームが代わりに各スキームの capability FD を介してリソースを開くというものです。
このとき、ネームスペーススキームが capability FD を持っていなければ、プロセスはスキームのリソースを開くことはできません。

おわりに

いかがでしたでしょうか? この記事では、私が Rust 製 OS、Redox OS で行った活動と Redox OS における今後について紹介しました。
この記事を通じて、Redox OS の開発とコントリビューションの興味深さが伝わればうれしいです!

ネームスペースマネージャ in ユーザスペースプロジェクトに関しても、また記事をお届けする予定ですので、どうぞご期待ください。
それでは、また次の記事でお会いしましょう!

Discussion