syscall: support process sandboxing using Landlock on Linux
要約
概要
syscall.SysProcAttr 構造体にLinuxのLandlock LSMを使ったプロセスサンドボックス機能を追加するproposal。子プロセスのみをサンドボックス化できるようにすることで、呼び出し元のGoプロセスに影響を与えずに最小権限原則を実現する。
ステータス変更
likely_accept → accepted
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_rulesetとlandlock_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システムが git や make などの外部コマンドを安全に実行する、(2) Webサーバーが画像変換ツールを一時ディレクトリのみのアクセス権で起動する、(3) PR_SET_NO_NEW_PRIVS を単独で設定することでsetuidプログラムの権限昇格を防ぐ、などが考えられる。
議論のハイライト
NoNewPrivsを独立フィールドにした理由: Landlockのlandlock_restrict_selfはno_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という既知の用語との対応を保つため却下された。