メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

#68595accepted

syscall: support process sandboxing using Landlock on Linux

ステータス変更: likely_accept accepted

要約

AIによる要約であり、誤りを含む場合があります。

概要

syscall.SysProcAttr 構造体にLinuxのLandlock LSMを使ったプロセスサンドボックス機能を追加するproposal。子プロセスのみをサンドボックス化できるようにすることで、呼び出し元のGoプロセスに影響を与えずに最小権限原則を実現する。

ステータス変更

likely_acceptaccepted
likely_accept となってから約1週間、LandlockFlags の型を uintptr から uint32 に変更すべきという指摘が挙がったが、提案者の @gnoack がコードレビュー段階で対応可能と説明し、コンセンサスに変化なしと判断された。2026-06-18に @aclements が正式にacceptedを宣言した。

技術的背景

現状の問題点

LinuxのLandlock LSM(Linux Security Module)は、特権不要のサンドボックス機構で、Linux 5.13以降で多くのディストリビューションでデフォルト有効になっている。しかしGoプログラムでサブプロセスのみにLandlockポリシーを適用したい場合、現状は呼び出し元のGoプロセス全体を同一サンドボックスに入れるしか方法がない。
たとえば「Goバイナリが信頼できないリポジトリを処理するために git を数十万回 os/exec で呼び出す」ケースでは、git には一時ディレクトリのみのアクセスを許可しつつ、呼び出し元はネットワークを含む広い権限を保持したい。GoのマルチスレッドランタイムによりLandlockのfork後適用が複雑で、既存の os/exec を再実装せずに実現する標準的な手段がなかった。
また fork(2)execve(2) の間でしか安全に実行できない処理を扱う forkAndExecInChild1() 関数に、Landlock呼び出しを追加する必要があるが、syscallパッケージは基本的にfreeze(凍結)状態であり、新しいシステムコール定数を zsysnum_linux_*.go に手動で追加する必要があるなど、技術的な障壁も存在していた。

提案された解決策

syscall.SysProcAttr 構造体(Linux限定)に4つのフィールドを追加する。

type SysProcAttr struct {
    // ... 既存フィールド ...
    // UseLandlock places the child into a Landlock restriction by
    // calling landlock_restrict_self(LandlockFD, LandlockFlags) before exec.
    //
    // Generally this should be combined with setting NoNewPrivs.
    UseLandlock   bool
    LandlockFD    int        // FD of a Landlock ruleset
    LandlockFlags uintptr    // Flags to landlock_restrict_self
    NoNewPrivs bool // call prctl(PR_SET_NO_NEW_PRIVS) before exec
}

内部実装では forkAndExecInChild1() 内で fork後・exec前に次を実行する。

  • NoNewPrivs が true の場合: prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
  • UseLandlock が true の場合: landlock_restrict_self(LandlockFD, LandlockFlags)
    Landlockポリシーのセットアップ(landlock_create_rulesetlandlock_add_rule)は呼び出し元のGoプロセスで事前に行い、得られたファイルディスクリプタを LandlockFD に渡す設計。これにより既存のエコシステムライブラリ(github.com/landlock-lsm/go-landlock 等)との組み合わせが容易になる。

これによって何ができるようになるか

Goプログラムが子プロセスのみを限定的なファイルアクセス権限で起動できるようになる。

コード例

// Before: 呼び出し元プロセス全体をサンドボックス化するしかなかった
// (子プロセスだけを制限する標準的な方法がなかった)
err := landlock.V5.BestEffort().Restrict(
    landlock.ROFiles("/usr"),
    landlock.RWFiles(tmpDir),
)
// この後に起動するすべてのプロセスが制限される(呼び出し元も含む)
// After: 子プロセスだけにLandlockポリシーを適用できる
rulesetFD, flags, err := landlock.V8.BestEffort().AsRuleset().Restrict(
    landlock.RODirs("/"),
    landlock.RWDirs("/tmp"),
)
if err != nil {
    // ...
}
defer syscall.Close(rulesetFD)
cmd := exec.Command("git", "clone", untrustedURL, tmpDir)
cmd.SysProcAttr = &syscall.SysProcAttr{
    NoNewPrivs:    true,
    UseLandlock:   true,
    LandlockFD:    rulesetFD,
    LandlockFlags: flags,
}
if err := cmd.Run(); err != nil {
    // ...
}
// 呼び出し元のGoプロセスのアクセス権限は変化しない

ユースケースとしては、(1) 信頼できないコードを処理するCIシステムが gitmake などの外部コマンドを安全に実行する、(2) Webサーバーが画像変換ツールを一時ディレクトリのみのアクセス権で起動する、(3) PR_SET_NO_NEW_PRIVS を単独で設定することでsetuidプログラムの権限昇格を防ぐ、などが考えられる。

議論のハイライト

  • NoNewPrivs を独立フィールドにした理由: Landlockの landlock_restrict_selfno_new_privs フラグが設定されているか CAP_SYS_ADMIN がなければ実行できないが、PR_SET_NO_NEW_PRIVS はLandlock以外(seccompなど)でも独立して有用なため、UseLandlock と分離された設計になった。@rsc の提案による。
  • 「汎用的なfork後syscall機構」の可能性: @aclements と @ianlancetaylor が「SysProcAttr にフィールドを追加し続けるのではなく、fork-exec間で任意のsyscallを指定できる汎用機構を検討すべきか」と議論したが、UidMappings のような複雑な処理との順序制御や型安全性の問題があり、今回のproposalはLandlock専用フィールド追加として進めた。
  • LandlockFlags の型: カーネルインターフェースが uint32 であるにもかかわらず uintptr が使われている点への指摘があった。コードレビューで解決できる問題として、proposalの承認は妨げないと判断された。
  • syscallパッケージのfreeze: zsysnum_linux_*.go は再生成不可の状態にあり、新しいシステムコール定数(SYS_LANDLOCK_RESTRICT_SELF 等)は手動で追加し、かつエクスポートしない方針が @ianlancetaylor から示された。
  • 後方互換性: NoNewPrivs のデフォルト値は false(=呼び出しなし)であり、既存コードの動作は一切変わらない。AllowNewPrivs bool のような反転フィールドにすべきという意見も出たが、Linuxの no_new_privs という既知の用語との対応を保つため却下された。

関連リンク