From 40450ec7f6af60684f483e3a041c6ffbfdc1fea1 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sat, 19 Sep 2026 05:11:30 +0000 Subject: [PATCH 1/2] fix: drop bwrap setuid, unmask /proc for bubblewrap sandbox - Dockerfile: remove chmod u+s on /usr/bin/bwrap - compose.yaml: unmask /proc via systempaths=unconfined so bubblewrap 0.12.0 can mount a fresh procfs without setuid - .claude/settings.json, .codex/config.toml: consolidate credential denies onto directory-level entries to avoid multi-file masking issues (openai/codex#43929) and drop the duplicate **/.env.* glob - .devcontainer/setup-devcontainer.sh: install the pnpm version pinned in packageManager before pnpm install, so sandboxed pnpm commands don't try to download a mismatched version - docs/dev-notes: plan/survey/learning for the bwrap setuid fix Co-Authored-By: Claude Sonnet 5 --- .claude/settings.json | 10 +- .codex/config.toml | 13 +- .devcontainer/setup-devcontainer.sh | 5 + Dockerfile | 1 - compose.yaml | 3 + .../2026-09-18/fix-bwrap-setuid/plan.md | 322 ++++++++++++++++++ .../2026-09-18/fix-bwrap-setuid/survey.md | 141 ++++++++ .../2026-09-19/fix-bwrap-setuid/learning.md | 29 ++ 8 files changed, 508 insertions(+), 16 deletions(-) create mode 100644 docs/dev-notes/2026-09-18/fix-bwrap-setuid/plan.md create mode 100644 docs/dev-notes/2026-09-18/fix-bwrap-setuid/survey.md create mode 100644 docs/dev-notes/2026-09-19/fix-bwrap-setuid/learning.md diff --git a/.claude/settings.json b/.claude/settings.json index 602db4f4e..b254c5746 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -2,14 +2,9 @@ "sandbox": { "enabled": true, "allowUnsandboxedCommands": false, + "enableWeakerNestedSandbox": true, "filesystem": { - "denyRead": [ - "~/.ssh", - "~/.claude/.credentials.json", - "~/.codex/auth.json", - "**/.env", - "**/.env.*" - ] + "denyRead": ["~/.ssh", "~/.claude/.credentials.json", "~/.codex/auth.json", "**/.env"] } }, "permissions": { @@ -30,7 +25,6 @@ "Read(~/.claude/.credentials.json)", "Read(~/.codex/auth.json)", "Read(**/.env)", - "Read(**/.env.*)", "Read(**/secrets/**)", "Read(**/config/credentials.json)", "Read(**/*.pem)", diff --git a/.codex/config.toml b/.codex/config.toml index 08e6abfc4..1825cb45d 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -7,19 +7,18 @@ extends = ":workspace" [permissions.project-edit.filesystem] glob_scan_max_depth = 8 -"~/.codex/auth.json" = "deny" -"~/.claude/.credentials.json" = "deny" +# openai/codex#43929: masking two or more files aborts the sandbox at startup, while directory +# denies are unaffected. Credentials are therefore denied by directory, keeping the single +# file slot for `.env`. +"~/.codex" = "deny" +"~/.claude" = "deny" "~/.ssh/**" = "deny" # Keep the denied set aligned with `.claude/settings.json`. Both files are committed and # therefore apply to host clones and cloud agents, not only to this devcontainer. [permissions.project-edit.filesystem.":workspace_roots"] +# The single file slot (see above). Listing `**/.env` too could count the same file twice. ".env" = "deny" -".env.*" = "deny" -# A leading `**/` does not match a top-level path in every glob implementation, so the -# bare and recursive forms are both listed on purpose. -"**/.env" = "deny" -"**/.env.*" = "deny" "**/secrets/**" = "deny" "**/config/credentials.json" = "deny" "**/*.pem" = "deny" diff --git a/.devcontainer/setup-devcontainer.sh b/.devcontainer/setup-devcontainer.sh index b54f1a157..78e9ebf36 100644 --- a/.devcontainer/setup-devcontainer.sh +++ b/.devcontainer/setup-devcontainer.sh @@ -32,5 +32,10 @@ rtk gain >/dev/null # Agent integration is optional and separate from installing the RTK CLI. rtk init -g --auto-patch || echo 'RTK init failed, continuing...' +# Match the global pnpm to `packageManager`; a mismatch makes pnpm download the pinned version, +# which the agent sandboxes cannot write, so every sandboxed `pnpm` command fails. +pnpm_version="$(node -p "require('./package.json').packageManager.split('@')[1].split('+')[0]")" +npm install -g "pnpm@${pnpm_version}" + # Install project dependencies pnpm install diff --git a/Dockerfile b/Dockerfile index 1264b1b27..bb6da7470 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,6 @@ COPY . /usr/src/app RUN apt-get update \ && apt-get -y install --no-install-recommends bubblewrap fish \ - && chmod u+s /usr/bin/bwrap \ && rm -rf /var/lib/apt/lists/* ENV NODE_PATH=/node_modules diff --git a/compose.yaml b/compose.yaml index 33624c0b9..f283efe1c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -13,6 +13,9 @@ services: security_opt: - apparmor=unconfined - seccomp=unconfined + # Unmask /proc so bubblewrap 0.12.0 can mount a fresh procfs without setuid; Codex's + # retry-without-/proc fallback misses the new error text (openai/codex#44329). + - systempaths=unconfined ports: - '5173:5173' - '5555:5555' diff --git a/docs/dev-notes/2026-09-18/fix-bwrap-setuid/plan.md b/docs/dev-notes/2026-09-18/fix-bwrap-setuid/plan.md new file mode 100644 index 000000000..77cebd708 --- /dev/null +++ b/docs/dev-notes/2026-09-18/fix-bwrap-setuid/plan.md @@ -0,0 +1,322 @@ +# Plan: devcontainer 内で Claude / Codex のサンドボックスを復旧する + +Issue: https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/4059 +調査結果: [survey.md](./survey.md) + +## 概要 + +- bubblewrap 0.12.0(Debian trixie の DSA-6472-1 で配信)は setuid での起動を拒否するため、`Dockerfile` の `chmod u+s /usr/bin/bwrap` で Claude / Codex の Bash がすべて失敗している。[1][2][3] +- setuid を外すと、隠れていた問題が 2 つ表に出る。 + - Claude: コンテナ内で新しい `/proc` をマウントできない(`Can't mount proc on /proc: Operation not permitted`)。[4] + - Codex: deny ルールに一致するファイルが 2 つ以上あると起動時に失敗する既知バグ(`Can't write data to file /usr/src/app/.env.example: Bad file descriptor`)。[5] +- 3 つを設定ファイルの変更だけで解消する。 + +## 設計判断 + +### setuid を外す + +- 上流の bubblewrap 0.12.0 は setuid 対応を削除しており、setuid 付きの bwrap は `acquire_privs()` で即終了する。[1][2] +- 残す選択肢はない。 + +### Claude: `sandbox.enableWeakerNestedSandbox: true` + +- Claude Code 公式ドキュメントの Troubleshooting に載っている、この症状への対処法そのもの。[4] + - "Set `enableWeakerNestedSandbox` to `true` so the inner sandbox bind-mounts the container's existing `/proc` instead." +- 設定リファレンス上の Scope は "Any file" で、プロジェクトの `.claude/settings.json` に書ける。[6] +- 公式ドキュメントの注意書きは "considerably weakens security and should only be used when additional isolation is otherwise enforced."。[4] + - この環境ではコンテナ自体が外側の隔離境界になっているため許容する。 + - ファイルシステムの deny とネットワーク制限は引き続き有効で、弱まるのはサンドボックス内のコマンドから `/proc` のプロセス情報が見えるようになる点。 +- Linux 専用の設定のため、macOS のホストで clone した場合は影響しない。Linux ホストで直接使う場合は同じく弱まる。 + +### Codex: `.env.example` を deny 対象から外す + +- openai/codex#43929 は未修正で、回避策も示されていない。[5] +- `.env.example` は `.gitignore` の `!.env.example` で commit 済みのテンプレートで、秘密情報を含まない。`git show` でも読めるため、deny しても守れるものがない。 +- `.env.*` を `.gitignore` と同じ意図の具体的なパターン(`.env.local`, `.env.*.local`)に置き換え、`.env.example` が一致しないようにする。 +- `.codex/config.toml` のコメントに従い、`.claude/settings.json` の deny も同じ集合に揃える。 + +## 却下した代替案 + +| 案 | 却下理由 | +| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `allowUnsandboxedCommands: true` / `sandbox.enabled: false` | 原因を直さずに防御を外すだけ。`denyRead` による認証情報の保護も効かなくなる | +| bubblewrap を 0.11.x に固定 | 廃止済みの setuid 方式を延命するだけで、CVE-2026-87766 の修正も受けられない [3] | +| `compose.yaml` に `security_opt: systempaths=unconfined` | Claude のサンドボックスの強度は保てるが、コンテナ全体で `/proc` のマスクと読み取り専用の保護が外れる(Phase 3d のトレードオフを参照)。Claude の公式ドキュメントの対処法でもない。※ Codex 用には Phase 3d で採用 | +| Codex の `.env.*` deny を残して上流の修正を待つ | 修正の目処が立っておらず、その間 Codex が起動しない | + +## Phase 1: `Dockerfile` から setuid を外す(低リスク) + +- レイヤー: 開発環境の設定(コンテナイメージ) +- 変更: `&& chmod u+s /usr/bin/bwrap \` の 1 行を削除する。 + +```dockerfile +RUN apt-get update \ + && apt-get -y install --no-install-recommends bubblewrap fish \ + && rm -rf /var/lib/apt/lists/* +``` + +- 確認: リビルド後に `ls -l /usr/bin/bwrap` の結果に `s` ビットが付いていないこと。 +- テスト: 分岐のある振る舞いを持たない設定変更のため、テストファーストは省略する。 + +## Phase 2: Claude の入れ子サンドボックスを有効にする(中リスク) + +- レイヤー: Claude Code 設定 +- 変更: `.claude/settings.json` の `sandbox` に `enableWeakerNestedSandbox: true` を追加する。 + +```json +{ + "sandbox": { + "enabled": true, + "allowUnsandboxedCommands": false, + "enableWeakerNestedSandbox": true, + "filesystem": { "...": "..." } + } +} +``` + +- 確認: Claude から `true`、`git status`、`git commit --dry-run` が成功すること。 + +## Phase 3: `.env.example` を deny 対象から外す(中リスク・秘密情報の扱いの変更) + +- レイヤー: Codex / Claude Code 設定 +- `.codex/config.toml` の `":workspace_roots"` テーブル: + +```toml +".env" = "deny" +".env.local" = "deny" +".env.*.local" = "deny" +"**/.env" = "deny" +"**/.env.local" = "deny" +"**/.env.*.local" = "deny" +``` + +- `.claude/settings.json`: `sandbox.filesystem.denyRead` と `permissions.deny` の `**/.env.*` / `Read(**/.env.*)` を同じ集合(`**/.env.local`, `**/.env.*.local`)に置き換える。 +- 確認: `codex sandbox -- /bin/true` が成功し、Claude / Codex から `.env` が読めず、`.env.example` が読めること。 +- 既知の制約: ワークスペースに `.env` と `.env.local` が同時に存在すると、再び openai/codex#43929 に当たる。現状は `.env` のみの想定。 + +## Phase 3b: deny を `.env` のみに絞る(Phase 3 の見直し) + +### 経緯 + +- Phase 3 の適用とリビルドのあと、Codex が `bwrap: Can't write data to file /usr/src/app/.env.local: Bad file descriptor` で失敗した。 +- `.env.local` は実在しない。リポジトリ直下の env ファイルは `.env` と `.env.example` のみ。 + +### 見直した原因の理解 + +- Phase 3 では「実在するファイルが 2 つ以上一致すると失敗する」と考えていたが、不十分だった。 +- `".env.local"` のようにワイルドカードを含まない完全パスは、ファイルが実在しなくても Codex がマスク対象として扱い、1 つに数えると考えられる。 +- 今回は `.env`(実在)と `.env.local`(実在しない完全パス)で 2 つになり、2 つ目で失敗した。 +- `".env.*.local"` のようなワイルドカードのパターンは、実在するファイルにしか一致しないと考えられる(未検証)。 + +| 時点 | 失敗したファイル | 一致したルール | +| -------------- | ---------------- | ------------------------------------ | +| Phase 3 適用前 | `.env.example` | `.env.*`(ワイルドカード、実在) | +| Phase 3 適用後 | `.env.local` | `.env.local`(完全パス、実在しない) | + +### 変更 + +- `.env.local` は運用上置いていないため、`.local` の系統を Codex / Claude の両方から外し、`.env` のみを deny する。 +- `.codex/config.toml` の `":workspace_roots"` テーブル: + +```toml +".env" = "deny" +"**/.env" = "deny" +``` + +- `.claude/settings.json`: `denyRead` と `permissions.deny` から `.env.local` / `.env.*.local` の行を外し、`**/.env` / `Read(**/.env)` のみ残す。 + +### 未確認の点とフォールバック + +- `".env"` と `"**/.env"` は同じ `.env` を指すが、Codex がこれを 1 つと数えるか 2 つと数えるかは未確認。 +- 2 つと数えられて失敗した場合は、`"**/.env"` を外して `".env"` のみにする。 + +### トレードオフ + +- 将来 `.env.local` などを置くと、Codex / Claude の deny から外れて読めるようになる。置く際に deny を足し直す必要がある(その時点で openai/codex#43929 が未修正なら Codex が起動しなくなる点にも注意)。 + +### 確認 + +- リビルド不要(設定ファイルのみ)。Codex CLI を再起動し、セッションが開始できること。 +- Codex / Claude から `.env` が読めず、`.env.example` が読めること。 + +## Phase 3c: Codex のファイル単位の deny を `.env` の 1 つに絞る(最優先) + +### 経緯 + +- Phase 3b のあと、Codex CLI が `bwrap: Can't write data to file /home/node/.claude/.credentials.json: Bad file descriptor` で起動しなくなった。 +- ファイル単位でマスクされる上限(1 つ)は、ワークスペース内だけでなく Codex の設定全体で数えられていた。 +- 現状でファイル単位にマスクされるのは `.env`、`~/.codex/auth.json`、`~/.claude/.credentials.json` の 3 つ(`~/.ssh` は存在せず、`**/*.pem` などに一致するファイルもない)。 + +### 判定: Codex CLI の不具合(設定ミスではない) + +- 公式ドキュメントは、完全パス(例: `~/.ssh`)とワイルドカード(例: `"**/*.env" = "deny"`)による deny を推奨している。現在の設定はこの使い方どおり。[7] +- openai/codex#43929 では、完全パスかワイルドカードかなどの条件によらず「deny に一致するファイルが 2 つ以上で必ず失敗、ディレクトリなら動く」と報告されている。0.155.1 でも未修正。[5] + +### 方針 + +- `.env` を守ることを最優先し、ファイル単位の 1 枠を `.env` に使う。 +- 認証ファイルはディレクトリ単位の deny に置き換える(#43929 によると、ディレクトリは上限に数えられない)。 + +| 対象 | 変更前 | 変更後 | +| ----------------------------- | ---------------------------------------- | ---------------------------------------------------------- | +| `.env` | `".env"` と `"**/.env"`(ファイル) | `".env"` のみ(同じファイルが 2 つと数えられるのを避ける) | +| `~/.claude/.credentials.json` | ファイル | `"~/.claude"`(ディレクトリ) | +| `~/.codex/auth.json` | ファイル | `"~/.codex"`(ディレクトリ) | +| `~/.ssh/**` | ワイルドカード(現在は何にも一致しない) | 変更なし | + +### リスクとフォールバック + +- `~/.codex` を deny すると、AGENTS.md を読み込むサンドボックスが `~/.codex` 配下(グローバルの AGENTS.md や skills)を読めず、失敗する可能性がある。 + - 失敗した場合は `"~/.codex"` の deny だけを外す。そうすると `~/.codex/auth.json` は Codex のコマンドから読めるようになるが、`.env` と `~/.claude` は守られたまま。 +- `"**/.env"` を外すため、サブディレクトリの `.env` は Codex の deny から外れる(現在は存在しない)。 +- `**/*.pem`、`**/*.key`、`**/secrets/**`、`**/config/credentials.json` に一致するファイルが今後 1 つでも置かれると、再び #43929 に当たる。 +- 次に #44304(bubblewrap 0.12.0 で `/proc` をマウントできないときの代替手段が働かない)に当たる可能性がある。[8] +- Claude 側の deny(`.claude/settings.json`)は変更しない。Claude にはこの制約がないため。 + +### 確認 + +- Codex CLI を再起動し、セッションが開始できること。 +- Codex から `.env`、`~/.claude/.credentials.json`、`~/.codex/auth.json` が読めず、`.env.example` が読めること。 + +## Phase 3d: `compose.yaml` に `systempaths=unconfined` を追加する(Codex の `/proc` 対策) + +### 経緯 + +- Phase 3c のあと、古い deny 設定を読み込んだまま動いていた VS Code 拡張の app server(PID 804、03:04 起動)を再起動した。すると Codex のエラーが `bwrap: Can't mount proc on /proc: Operation not permitted` に変わった。 +- deny の変更は反映された。次の問題として、[8] の `/proc` の問題が表に出た。 + +### 原因(根拠) + +- Codex 公式の secure devcontainer は「bubblewrap を setuid で入れる」方式で、Docker 内で `bwrap --proc /proc` が拒否されたときは、Codex が `--proc` なしで再試行する設計になっている。[9] +- bubblewrap 0.12.0 では setuid 方式が使えず、エラー文言も `/newroot/proc` から `/proc` に変わった。Codex は `/newroot/proc` という文字列で失敗を判定しているため、再試行されない。[8][10] +- #44304 と #44329 はどちらも Open で、PR もない。 +- Codex 公式ドキュメント(Permissions)には、コンテナ内での `/proc` の扱いについての記述がない。[7] + +### 変更 + +- `compose.yaml` の `web.security_opt` に `systempaths=unconfined` を追加する。 +- Docker が `/proc` の一部をマスクするのをやめるので、user namespace の中でも bwrap が新しい `/proc` をマウントできるようにする狙い(未検証)。 +- コンテナの作り直しが必要(Rebuild Container)。 + +### トレードオフ + +- Docker 公式の説明は "Turn off confinement for system paths (masked paths, read-only paths) for the container"。[11] 影響は次の 2 つで、コンテナ内の全プロセスが対象になる。 + - マスクの解除: Docker が隠していた `/proc` のパス(`/proc/kcore` など)が見えるようになる。 + - 読み取り専用の解除: `/proc/sys`、`/proc/sysrq-trigger` などが書き込めるようになる。こちらのほうが影響が大きい。 +- このコンテナは user namespace で root を分離していない(userns-remap なし)。そのため、コンテナ内で root(`sudo`)になると、ホストのカーネル設定を変えたり、`/proc/sysrq-trigger` でホストを再起動したりできる経路が開く。 + +#### 実害の範囲(ローカルの OrbStack で動かす場合) + +- ここでの「ホストのカーネル」は、macOS ではなく OrbStack が動かしている Linux VM のカーネル。影響が及ぶのはこの VM と、同じ VM で動くほかのコンテナまでで、Mac 本体のファイルには届かない。 +- 悪意のあるコードがコンテナ内で root になれた場合に、新たにできるようになること: + +| できるようになること | 実害 | +| -------------------------------- | -------------------------------------------------------------------------------------- | +| `/proc/sysrq-trigger` に書き込む | VM を即座に再起動・停止できる。OrbStack の全コンテナが止まる | +| `/proc/sys` に書き込む | VM のカーネル設定を変えられる。ほかのコンテナの保護を弱めることもできる | +| `/proc/kcore` を読む | VM のカーネルメモリを読める。ほかのコンテナの秘密情報(DB のパスワードなど)が漏れうる | + +- root でなくても、`/proc/timer_list` などから VM 内のプロセスの情報が少し見えるようになる(軽微)。 + +#### 誰がこれをできるか + +- devcontainer の `node` ユーザーは、通常パスワードなしで `sudo` を使える。`pnpm install` で入ったパッケージの postinstall スクリプトなど、サンドボックスの外で動くコードが悪意を持っていれば、root になれる。 +- Claude / Codex がサンドボックスの中で実行するコマンドは、bwrap が `sudo` による昇格を防ぐため、この経路は通らない。 + +#### 判断 + +- このコンテナにはすでに `SYS_ADMIN` と `seccomp` / `apparmor` の unconfined が与えられており、root ならこれらの保護を自分で外せる。`systempaths=unconfined` で新しくできるようになることはほとんどなく、変わるのは攻撃に必要な手間が 1 段減ること。 +- ただし「すでに緩めてあるから、さらに緩めてもよい」という理屈は、緩和を重ねる理由にはならない点に注意する。 +- ローカルの開発環境で、同じ OrbStack の VM に重要なコンテナを同居させない前提で許容する。この前提と実害の範囲は、チームに共有する。 +- Phase 2 の「却下した代替案」では Claude 用として退けたが、Codex には代わりの手段がないため採用する。Claude の `enableWeakerNestedSandbox` は、効果を確認できるまで残す。 + +### フォールバック + +- それでも `/proc` のエラーが出る場合は、この変更を戻し、上流の修正を待つ。 +- 上流の修正を待つ間は、`default_permissions` を外して組み込みの `:workspace` で動かす(`.env` が Codex から読めるようになる)かどうかを、チームで判断する。 + +### 確認 + +- Rebuild Container のあと、Codex CLI と VS Code 拡張の両方で、セッションが開始できること。 +- Codex から `.env`、`~/.claude/.credentials.json`、`~/.codex/auth.json` が読めず、`.env.example` が読めること。 +- Claude の Bash が引き続き動くこと。 + +## Phase 3e: コンテナの pnpm を `packageManager` に揃える + +### 経緯 + +- Codex が動いたあと、Claude CLI から commit できなかった。lefthook の `format` ジョブ(`pnpm exec prettier`)がサンドボックス内で `create the temporary package manager install directory: Read-only file system` で失敗していた。 +- `pnpm test:unit` や `pnpm lint` など、Claude がサンドボックス内で実行する `pnpm` はすべて同じ理由で失敗する。 + +### 原因 + +- pnpm 11 以降は、実行中の pnpm と `package.json` の `packageManager` のバージョンが違うと、指定のバージョンを自動でダウンロードして使う(`pmOnFail: download` が既定)。[12] +- コンテナの pnpm はベースイメージ(`mcr.microsoft.com/devcontainers/javascript-node:24`)に入っている 12.3.4。`packageManager` は Renovate が上げていて 12.4.2。 +- サンドボックスの中ではダウンロード先に書き込めないため失敗する。サンドボックスの外では黙ってダウンロードして動くため、これまで気づかなかった。 +- `Dockerfile` と `setup-devcontainer.sh` は、過去に一度も pnpm のバージョンを固定していない(git 履歴で確認)。 + +| 日付 | `packageManager` | コンテナの 12.3.4 との関係 | +| ----- | ---------------- | -------------------------- | +| 09-05 | pnpm@12.3.4 | 一致 | +| 09-13 | pnpm@12.4.1 | ここからずれた | +| 09-18 | pnpm@12.4.2 | ずれたまま | + +- ずれ始めた 09-13 は、#4033 / #4034 で Claude の Bash が全滅した時期と重なる。そのため、サンドボックスを直すまで表に出なかった。 +- 09-01 以前(pnpm 11 の時期)に問題にならなかった理由は、当時のイメージの pnpm の版が分からず未確認。 + +### 変更(案 A) + +- `.devcontainer/setup-devcontainer.sh` の `pnpm install` の直前で、`package.json` の `packageManager` と同じ版の pnpm をグローバルに入れる。 +- バージョンを 2 か所に書かないので、Renovate が `packageManager` を上げても、次のリビルドで自動的に追従する。 + +```bash +# Match the global pnpm to `packageManager`; a mismatch makes pnpm download the pinned version, +# which the agent sandboxes cannot write, so every sandboxed `pnpm` command fails. +pnpm_version="$(node -p "require('./package.json').packageManager.split('@')[1].split('+')[0]")" +npm install -g "pnpm@${pnpm_version}" +``` + +- `+sha512...` のハッシュが `packageManager` に付いた場合に備え、`+` 以降は取り除く。 +- 失敗したときは、後続の `pnpm install` と同じくセットアップ全体を失敗させる(黙ってずれたままにしない)。 + +### 却下した代替案 + +| 案 | 却下理由 | +| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| サンドボックスで pnpm のキャッシュへの書き込みを許可(`allowWrite`) | サンドボックスを緩める。書き込めるようになった pnpm 本体を書き換えられる余地が生まれる | +| `Dockerfile` に pnpm の版を直接書く | `package.json` と 2 か所で管理することになり、Renovate の更新でまたずれる | +| `pmOnFail: ignore` にしてダウンロードさせない | `packageManager` による版の固定が効かなくなる | + +### 確認 + +- Rebuild Container のあと、`pnpm --version` が `packageManager` と一致すること。 +- Claude CLI から `pnpm exec prettier --version` と `git commit` が通ること。 +- 今すぐの回避(リビルド前): ご自身のターミナルで `npm install -g pnpm@12.4.2`。 + +## Phase 4: リビルドと検証(高リスク・開発環境全体に影響) + +- devcontainer を Rebuild Container する。 +- Claude / Codex の両方で次を確認する。 + - `git commit` まで通ること + - `.env`、`~/.codex/auth.json`、`~/.claude/.credentials.json` が読めないこと +- `pnpm format`、`pnpm lint`、`git diff --check` を実行する。 + +## レビュー + +- 秘密情報の扱いの変更(deny 設定)を含むため、AGENTS.md に従いクロスレビューの対象とする。 +- Claude 主導の変更なので Codex でレビューする。Codex が使えない場合は `coderabbit review --plain` を使う。 + +## 出典 + +1. bubblewrap releases(0.11.2 で setuid を非推奨化、0.12.0 で削除): https://github.com/containers/bubblewrap/releases +2. bubblewrap source(`acquire_privs()` の `die ("setuid use of bubblewrap is not supported")`): https://github.com/containers/bubblewrap/blob/main/bubblewrap.c +3. Debian security tracker(DSA-6472-1 / CVE-2026-87766、trixie で 0.12.0-1~deb13u1): https://security-tracker.debian.org/tracker/DSA-6472-1 / https://security-tracker.debian.org/tracker/CVE-2026-87766 +4. Claude Code sandboxing(Troubleshooting「Bubblewrap fails to start inside a container」、Security limitations): https://code.claude.com/docs/en/sandboxing +5. openai/codex#43929(deny ルールに一致するファイルが 2 つ以上あると起動に失敗): https://github.com/openai/codex/issues/43929 +6. Claude Code settings reference(`sandbox.enableWeakerNestedSandbox`、Scope: Any file): https://code.claude.com/docs/en/settings-reference +7. Codex Permissions(deny の完全パスとワイルドカード、`:workspace_roots`、`glob_scan_max_depth`): https://learn.chatgpt.com/docs/permissions +8. openai/codex#44304(bubblewrap 0.12.0 で `/proc` のエラー文言が変わり、代替手段が働かない): https://github.com/openai/codex/issues/44304 +9. openai/codex PR #17547(secure devcontainer で bubblewrap を setuid で入れる。`/proc` のマウントが拒否されたら `--proc` なしで再試行): https://github.com/openai/codex/pull/17547 +10. openai/codex#44329(CLI と VS Code 拡張が `/proc` のマウント失敗を扱えない): https://github.com/openai/codex/issues/44329 +11. Docker `docker container run`(`--security-opt systempaths=unconfined`): https://docs.docker.com/reference/cli/docker/container/run/ +12. pnpm 11.0 release notes(`pmOnFail`、既定は `download`): https://pnpm.io/blog/releases/11.0 diff --git a/docs/dev-notes/2026-09-18/fix-bwrap-setuid/survey.md b/docs/dev-notes/2026-09-18/fix-bwrap-setuid/survey.md new file mode 100644 index 000000000..a007ed991 --- /dev/null +++ b/docs/dev-notes/2026-09-18/fix-bwrap-setuid/survey.md @@ -0,0 +1,141 @@ +# Survey: #4033 以降に Claude / Codex から commit できない問題 + +Issue: https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/4059 + +## 症状 + +- Claude Code / Codex 経由の Bash コマンドがすべて `bwrap: setuid use of bubblewrap is not supported` で失敗する。 +- `git commit` だけでなく `gh`・`ls` なども含め、Bash が一切動かない。 + +## 結論 + +- Debian stable のセキュリティ更新で bubblewrap 0.12.0 が入り、setuid で bwrap を動かす方式が上流で廃止されていた。 +- `Dockerfile` の `chmod u+s /usr/bin/bwrap` がこの廃止済みの使い方に該当し、コンテナの再ビルド後に bwrap が起動直後に終了するようになった。 +- 設定ミスというより、上流の方針変更とバージョン未固定の `apt-get install` が重なったことが原因。 + +```dockerfile +RUN apt-get update \ + && apt-get -y install --no-install-recommends bubblewrap fish \ + && chmod u+s /usr/bin/bwrap \ + && rm -rf /var/lib/apt/lists/* +``` + +## 根本原因 + +### 1. bwrap の 2 つの動作方式 + +| 方式 | 仕組み | 現状 | +| ------------------- | ---------------------------------------------------------------------------- | ---------- | +| user namespace 方式 | 一般ユーザーのままカーネルの unprivileged user namespace で namespace を作る | 現在の標準 | +| setuid 方式 | `chmod u+s` で root 権限付きで起動し、その権限で namespace を作る | 廃止 | + +- Claude Code 公式ドキュメントも bubblewrap を "the unprivileged sandboxing tool" と説明している。 +- setuid 方式は user namespace が使えない古いカーネル向けの後方互換手段だった。 + +### 2. 上流 bubblewrap での廃止(公式リリースノート) + +- 0.11.2(2026-04): CVE-2026-41163(setuid で入れた bwrap に ptrace で割り込み、サンドボックス準備処理を乗っ取れる)を受けて setuid を非推奨化した。 + - 新しいビルドオプション `-Dsupport_setuid` の既定値は false で、"Binaries built with this will refuse to run if made setuid." +- 0.12.0(2026-08-26): setuid 対応を完全に削除した。 + - "This version removes the support for building a setuid bubblewrap. ... basically all modern linux distributions now support unprivileged user namespaces" + +今回のエラーは `bubblewrap.c` の `acquire_privs()` から出ている。 + +```c +/* Are we setuid ? */ +if (real_uid != euid) + { + /* Historically we supported this, but now we only do user namespaces */ + die ("setuid use of bubblewrap is not supported"); + } +``` + +- setuid ビットが付いていると実 UID(node)と実効 UID(root)が食い違い、bwrap はそれを検知して即終了する。 + +### 3. 私たちの環境に入った経路(Debian security tracker) + +- 2026-08-27: DSA-6472-1 で trixie-security に `bubblewrap 0.12.0-1~deb13u1` が配信された。 + - 修正対象は CVE-2026-87766(サンドボックス準備中に親ディレクトリのシンボリックリンクをたどり、ホスト側へ書き込めてしまう)。 + - Debian は stable にもかかわらずメジャー更新の 0.12.0 をそのまま入れた(bookworm には修正が大がかりすぎるとして backport されていない)。 +- `Dockerfile` は `apt-get install bubblewrap` でバージョンを固定していない。 + - そのため、ビルドした日によって 0.11.x(setuid で動く)と 0.12.0(setuid だと即終了)のどちらが入るかが変わる。 + +### 4. Bash が全滅する理由(Claude Code 公式ドキュメント) + +- Linux では Bash コマンドを 1 つずつ bwrap で包んで実行する。 +- `.claude/settings.json` は `allowUnsandboxedCommands: false`(公式ドキュメント上は Strict sandbox mode)。 + - この設定では "every command Claude runs must run sandboxed" となり、サンドボックスの外で再実行する逃げ道がない。 +- よって bwrap の起動自体が失敗すると、個別コマンドの許可設定とは無関係にすべての Bash が失敗する。 +- Codex も同じ `/usr/bin/bwrap` を使うため、両方同時に影響を受ける。 + +### 5. setuid が実際に回避していたもの(当初の「setuid は不要」は誤り) + +- setuid を外すと、Claude の Bash は `bwrap: Can't mount proc on /proc: Operation not permitted` で失敗するようになった。 +- user namespace 方式では、カーネルは既存の `/proc` がすべて見えている場合にしか新しい procfs のマウントを許さない。 +- Docker は `/proc/kcore` などをマスク(上書きマウント)しているため、この条件を満たせず EPERM になる。 +- setuid 方式では bwrap がコンテナの root 権限(`SYS_ADMIN`)で動くため、この制約を受けずに `/proc` をマウントできていた。 +- Claude Code 公式ドキュメントの Troubleshooting にも同じ症状が載っている: "in an unprivileged container, bubblewrap can't mount a fresh `/proc` filesystem ... Set `enableWeakerNestedSandbox` to `true` so the inner sandbox bind-mounts the container's existing `/proc` instead." +- つまり setuid は「コンテナ内で `/proc` をマウントする」ための回避策として機能していた。 + +## 現環境での確認結果(2026-09-19) + +```bash +$ cat /etc/debian_version; bwrap --version +13.6 +bubblewrap 0.12.0 +``` + +- Debian 13(trixie)上で bubblewrap 0.12.0 が入っていることを確認した。上記の根本原因と一致する。 + +## #4034 時点で動いていた理由(推測) + +- #4034 のマージは 2026-09-13 で、DSA の配信(2026-08-27)より後。 +- PR の作業中(8/27 より前)にビルドしたイメージ、またはビルドキャッシュを使っていたため、setuid 対応の 0.11.x が残っていたと推測している。 + +## setuid を外した後に出た 2 つのエラー + +setuid を外すと、Claude と Codex でそれぞれ別の問題が表に出た。 + +### Claude: `Can't mount proc on /proc: Operation not permitted` + +- 原因は上記 5。コンテナ内で新しい `/proc` をマウントできない。 +- 案 A(公式ドキュメントの対処): `.claude/settings.json` に `sandbox.enableWeakerNestedSandbox: true` を追加する。 + - bwrap は新しい `/proc` をマウントせず、コンテナの `/proc` を bind mount する。 + - 公式ドキュメントの注意書き: "considerably weakens security and should only be used when additional isolation is otherwise enforced." この環境ではコンテナ自体が外側の隔離境界になる。 +- 案 B: `compose.yaml` の `security_opt` に `systempaths=unconfined` を追加し、Docker による `/proc` のマスクをやめる。 + - Claude のサンドボックスの強度は保てるが、コンテナ内の全プロセスからマスク対象だった `/proc` のパスが見えるようになる。 + +### Codex: `Can't write data to file /usr/src/app/.env.example: Bad file descriptor` + +- Codex 側の既知の未修正バグ openai/codex#43929: ワークスペース直下で deny ルールにマッチするファイルが 2 つ以上あると、Codex が起動時に失敗する(0.152.1 / 0.153.4 で再現が報告されている)。 +- `.codex/config.toml` の `.env` / `.env.*` に、`.env` と `.env.example` の 2 ファイルがマッチしている。 +- bwrap の setuid 問題とは別件で、setuid の問題で起動前に止まっていたため今まで見えていなかったと推測している(#4034 の時点で動いていた理由は未確認)。 +- 対処候補: 上流の修正を待つか、deny にマッチする実ファイルを 1 つ以下に絞る(例: 秘密情報を含まないテンプレートの `.env.example` を deny 対象から外す)。 + +## 修正方針 + +- `Dockerfile` から `&& chmod u+s /usr/bin/bwrap \` の 1 行を削除する(bubblewrap 0.12.0 では setuid 方式自体が使えないため必須)。 +- Claude: 案 A または案 B で `/proc` の問題に対処する。 +- Codex: openai/codex#43929 への対処を決める。 + +## 却下した代替案 + +- `allowUnsandboxedCommands: true` にする: サンドボックスの外での実行を許すことになり、`denyRead` による認証情報の保護が弱まる。 +- `sandbox.enabled: false` にする: 同上。原因を直さずに防御を外すだけになる。 +- bubblewrap を 0.11.x に固定する: 廃止済みの setuid 方式を延命するだけで、CVE-2026-87766 の修正も受けられなくなる。 + +## 未確認事項 + +- 修正後に Claude / Codex の両方で `git commit` が通ることを確認する必要がある。 + +## 参考 + +- bubblewrap releases: https://github.com/containers/bubblewrap/releases +- bubblewrap source (`acquire_privs`): https://github.com/containers/bubblewrap/blob/main/bubblewrap.c +- Debian tracker: https://tracker.debian.org/pkg/bubblewrap +- DSA-6472-1: https://security-tracker.debian.org/tracker/DSA-6472-1 +- CVE-2026-41163: https://security-tracker.debian.org/tracker/CVE-2026-41163 +- CVE-2026-87766: https://security-tracker.debian.org/tracker/CVE-2026-87766 +- Claude Code sandboxing: https://code.claude.com/docs/en/sandboxing +- Claude Code settings reference(`sandbox.enableWeakerNestedSandbox`): https://code.claude.com/docs/en/settings-reference +- openai/codex#43929: https://github.com/openai/codex/issues/43929 diff --git a/docs/dev-notes/2026-09-19/fix-bwrap-setuid/learning.md b/docs/dev-notes/2026-09-19/fix-bwrap-setuid/learning.md new file mode 100644 index 000000000..9860d0706 --- /dev/null +++ b/docs/dev-notes/2026-09-19/fix-bwrap-setuid/learning.md @@ -0,0 +1,29 @@ +# Learning: devcontainer 内で Claude / Codex のサンドボックスが全滅した件 + +## 問題 + +- 症状: devcontainer 内で Claude / Codex の Bash が全滅し、`git commit` もできない(`bwrap: setuid use of bubblewrap is not supported` → `/proc` マウント失敗 → Codex の `Bad file descriptor`)。 +- 根本原因: Debian のセキュリティ更新で入った bubblewrap 0.12.0 が setuid 方式を廃止し、setuid が暗黙に担っていた `/proc` マウントの回避策が消えた。その結果、Codex 側の未修正バグ 2 件(ファイル単位の deny が 2 つ以上で失敗、`/proc` 失敗時の代替手段が新しいエラー文言を検知できない)が順に表に出た。 + +## 有効だったアプローチ + +- エラー文言で上流のソース(`bubblewrap.c` の `acquire_privs()`)とリリースノート、Debian security tracker を引き、「いつ・なぜ挙動が変わったか」を一次情報で確定させた。 +- エラーの対象パスが変わるたびに「どの設定行が効いているか」を実ファイル(`ls`、`find`)と突き合わせ、仮説を 1 つずつ潰した。 +- 設定が反映されないときは、常駐プロセスの起動時刻と設定ファイルの更新時刻を比べた(VS Code 拡張が起動した Codex の app server が、古い設定のまま CLI の接続先になっていた)。 +- Claude は公式ドキュメントの `enableWeakerNestedSandbox`、Codex は `systempaths=unconfined` で `/proc` のマウントを通し、実害の範囲(OrbStack の VM 内に閉じる)を明記して許容した。 + +## ハマった点 + +- 「setuid は不要」と判断した: setuid が `/proc` マウントの回避策を兼ねていたことを見落としていた。 +- 「ワークスペース内の実在ファイルが 2 つ以上で失敗」と狭く理解した: 実際は、実在しない完全パスも含めて、Codex の設定全体で数えられていた。そのため `.env.example` → `.env.local` → `~/.claude/.credentials.json` と、3 回に分けて潰すことになった。 +- 「CLI を再起動すれば反映される」と説明した: CLI は常駐している app server に接続するだけで、設定を読み直していなかった。 +- `.env` を deny から外す案を推した: 「動かすこと」を優先して目的(秘密情報の保護)を損ねる本末転倒だった。 +- 「すでに緩めてあるから」を理由に `systempaths=unconfined` のリスクを小さく見積もった: 緩和を重ねる理由にはならない。 + +## 教訓 + +- 権限まわりの設定(setuid、capability、security_opt)を外すときは、それが暗黙に回避していた制約を先に洗い出す。例: setuid が `/proc` マウントの制約を回避していた。 +- バージョンを固定しないパッケージは、再ビルドした日によって挙動が変わる。「以前は動いた」ときは、まず実環境のバージョンを確認する。例: `bwrap --version` で 0.12.0 を確認。 +- 設定の変更が効かないときは、その設定を読むのが常駐プロセスかどうかを確かめ、プロセスの起動時刻を設定の更新時刻と比べる。 +- 回避策を選ぶときは、守りたいもの(今回は `.env` と認証情報)を先に固定し、それを削る案は候補から外す。 +- セキュリティの緩和は、既存の緩和を根拠にせず、単体で実害(誰が・何を・どこまで)を書き出してから判断する。 From a3a1aff731c3fafc2f887bf7449e8ba51c882193 Mon Sep 17 00:00:00 2001 From: "k.hiro1818" Date: Sat, 19 Sep 2026 05:43:06 +0000 Subject: [PATCH 2/2] docs: consolidate bwrap setuid dev-notes into a single summary Merge plan.md, survey.md and learning.md into summary.md, keeping the decisions, trade-offs, root causes of the detours and primary sources. Co-Authored-By: Claude Opus 5 --- .../2026-09-18/fix-bwrap-setuid/plan.md | 322 ------------------ .../2026-09-18/fix-bwrap-setuid/summary.md | 141 ++++++++ .../2026-09-18/fix-bwrap-setuid/survey.md | 141 -------- .../2026-09-19/fix-bwrap-setuid/learning.md | 29 -- 4 files changed, 141 insertions(+), 492 deletions(-) delete mode 100644 docs/dev-notes/2026-09-18/fix-bwrap-setuid/plan.md create mode 100644 docs/dev-notes/2026-09-18/fix-bwrap-setuid/summary.md delete mode 100644 docs/dev-notes/2026-09-18/fix-bwrap-setuid/survey.md delete mode 100644 docs/dev-notes/2026-09-19/fix-bwrap-setuid/learning.md diff --git a/docs/dev-notes/2026-09-18/fix-bwrap-setuid/plan.md b/docs/dev-notes/2026-09-18/fix-bwrap-setuid/plan.md deleted file mode 100644 index 77cebd708..000000000 --- a/docs/dev-notes/2026-09-18/fix-bwrap-setuid/plan.md +++ /dev/null @@ -1,322 +0,0 @@ -# Plan: devcontainer 内で Claude / Codex のサンドボックスを復旧する - -Issue: https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/4059 -調査結果: [survey.md](./survey.md) - -## 概要 - -- bubblewrap 0.12.0(Debian trixie の DSA-6472-1 で配信)は setuid での起動を拒否するため、`Dockerfile` の `chmod u+s /usr/bin/bwrap` で Claude / Codex の Bash がすべて失敗している。[1][2][3] -- setuid を外すと、隠れていた問題が 2 つ表に出る。 - - Claude: コンテナ内で新しい `/proc` をマウントできない(`Can't mount proc on /proc: Operation not permitted`)。[4] - - Codex: deny ルールに一致するファイルが 2 つ以上あると起動時に失敗する既知バグ(`Can't write data to file /usr/src/app/.env.example: Bad file descriptor`)。[5] -- 3 つを設定ファイルの変更だけで解消する。 - -## 設計判断 - -### setuid を外す - -- 上流の bubblewrap 0.12.0 は setuid 対応を削除しており、setuid 付きの bwrap は `acquire_privs()` で即終了する。[1][2] -- 残す選択肢はない。 - -### Claude: `sandbox.enableWeakerNestedSandbox: true` - -- Claude Code 公式ドキュメントの Troubleshooting に載っている、この症状への対処法そのもの。[4] - - "Set `enableWeakerNestedSandbox` to `true` so the inner sandbox bind-mounts the container's existing `/proc` instead." -- 設定リファレンス上の Scope は "Any file" で、プロジェクトの `.claude/settings.json` に書ける。[6] -- 公式ドキュメントの注意書きは "considerably weakens security and should only be used when additional isolation is otherwise enforced."。[4] - - この環境ではコンテナ自体が外側の隔離境界になっているため許容する。 - - ファイルシステムの deny とネットワーク制限は引き続き有効で、弱まるのはサンドボックス内のコマンドから `/proc` のプロセス情報が見えるようになる点。 -- Linux 専用の設定のため、macOS のホストで clone した場合は影響しない。Linux ホストで直接使う場合は同じく弱まる。 - -### Codex: `.env.example` を deny 対象から外す - -- openai/codex#43929 は未修正で、回避策も示されていない。[5] -- `.env.example` は `.gitignore` の `!.env.example` で commit 済みのテンプレートで、秘密情報を含まない。`git show` でも読めるため、deny しても守れるものがない。 -- `.env.*` を `.gitignore` と同じ意図の具体的なパターン(`.env.local`, `.env.*.local`)に置き換え、`.env.example` が一致しないようにする。 -- `.codex/config.toml` のコメントに従い、`.claude/settings.json` の deny も同じ集合に揃える。 - -## 却下した代替案 - -| 案 | 却下理由 | -| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `allowUnsandboxedCommands: true` / `sandbox.enabled: false` | 原因を直さずに防御を外すだけ。`denyRead` による認証情報の保護も効かなくなる | -| bubblewrap を 0.11.x に固定 | 廃止済みの setuid 方式を延命するだけで、CVE-2026-87766 の修正も受けられない [3] | -| `compose.yaml` に `security_opt: systempaths=unconfined` | Claude のサンドボックスの強度は保てるが、コンテナ全体で `/proc` のマスクと読み取り専用の保護が外れる(Phase 3d のトレードオフを参照)。Claude の公式ドキュメントの対処法でもない。※ Codex 用には Phase 3d で採用 | -| Codex の `.env.*` deny を残して上流の修正を待つ | 修正の目処が立っておらず、その間 Codex が起動しない | - -## Phase 1: `Dockerfile` から setuid を外す(低リスク) - -- レイヤー: 開発環境の設定(コンテナイメージ) -- 変更: `&& chmod u+s /usr/bin/bwrap \` の 1 行を削除する。 - -```dockerfile -RUN apt-get update \ - && apt-get -y install --no-install-recommends bubblewrap fish \ - && rm -rf /var/lib/apt/lists/* -``` - -- 確認: リビルド後に `ls -l /usr/bin/bwrap` の結果に `s` ビットが付いていないこと。 -- テスト: 分岐のある振る舞いを持たない設定変更のため、テストファーストは省略する。 - -## Phase 2: Claude の入れ子サンドボックスを有効にする(中リスク) - -- レイヤー: Claude Code 設定 -- 変更: `.claude/settings.json` の `sandbox` に `enableWeakerNestedSandbox: true` を追加する。 - -```json -{ - "sandbox": { - "enabled": true, - "allowUnsandboxedCommands": false, - "enableWeakerNestedSandbox": true, - "filesystem": { "...": "..." } - } -} -``` - -- 確認: Claude から `true`、`git status`、`git commit --dry-run` が成功すること。 - -## Phase 3: `.env.example` を deny 対象から外す(中リスク・秘密情報の扱いの変更) - -- レイヤー: Codex / Claude Code 設定 -- `.codex/config.toml` の `":workspace_roots"` テーブル: - -```toml -".env" = "deny" -".env.local" = "deny" -".env.*.local" = "deny" -"**/.env" = "deny" -"**/.env.local" = "deny" -"**/.env.*.local" = "deny" -``` - -- `.claude/settings.json`: `sandbox.filesystem.denyRead` と `permissions.deny` の `**/.env.*` / `Read(**/.env.*)` を同じ集合(`**/.env.local`, `**/.env.*.local`)に置き換える。 -- 確認: `codex sandbox -- /bin/true` が成功し、Claude / Codex から `.env` が読めず、`.env.example` が読めること。 -- 既知の制約: ワークスペースに `.env` と `.env.local` が同時に存在すると、再び openai/codex#43929 に当たる。現状は `.env` のみの想定。 - -## Phase 3b: deny を `.env` のみに絞る(Phase 3 の見直し) - -### 経緯 - -- Phase 3 の適用とリビルドのあと、Codex が `bwrap: Can't write data to file /usr/src/app/.env.local: Bad file descriptor` で失敗した。 -- `.env.local` は実在しない。リポジトリ直下の env ファイルは `.env` と `.env.example` のみ。 - -### 見直した原因の理解 - -- Phase 3 では「実在するファイルが 2 つ以上一致すると失敗する」と考えていたが、不十分だった。 -- `".env.local"` のようにワイルドカードを含まない完全パスは、ファイルが実在しなくても Codex がマスク対象として扱い、1 つに数えると考えられる。 -- 今回は `.env`(実在)と `.env.local`(実在しない完全パス)で 2 つになり、2 つ目で失敗した。 -- `".env.*.local"` のようなワイルドカードのパターンは、実在するファイルにしか一致しないと考えられる(未検証)。 - -| 時点 | 失敗したファイル | 一致したルール | -| -------------- | ---------------- | ------------------------------------ | -| Phase 3 適用前 | `.env.example` | `.env.*`(ワイルドカード、実在) | -| Phase 3 適用後 | `.env.local` | `.env.local`(完全パス、実在しない) | - -### 変更 - -- `.env.local` は運用上置いていないため、`.local` の系統を Codex / Claude の両方から外し、`.env` のみを deny する。 -- `.codex/config.toml` の `":workspace_roots"` テーブル: - -```toml -".env" = "deny" -"**/.env" = "deny" -``` - -- `.claude/settings.json`: `denyRead` と `permissions.deny` から `.env.local` / `.env.*.local` の行を外し、`**/.env` / `Read(**/.env)` のみ残す。 - -### 未確認の点とフォールバック - -- `".env"` と `"**/.env"` は同じ `.env` を指すが、Codex がこれを 1 つと数えるか 2 つと数えるかは未確認。 -- 2 つと数えられて失敗した場合は、`"**/.env"` を外して `".env"` のみにする。 - -### トレードオフ - -- 将来 `.env.local` などを置くと、Codex / Claude の deny から外れて読めるようになる。置く際に deny を足し直す必要がある(その時点で openai/codex#43929 が未修正なら Codex が起動しなくなる点にも注意)。 - -### 確認 - -- リビルド不要(設定ファイルのみ)。Codex CLI を再起動し、セッションが開始できること。 -- Codex / Claude から `.env` が読めず、`.env.example` が読めること。 - -## Phase 3c: Codex のファイル単位の deny を `.env` の 1 つに絞る(最優先) - -### 経緯 - -- Phase 3b のあと、Codex CLI が `bwrap: Can't write data to file /home/node/.claude/.credentials.json: Bad file descriptor` で起動しなくなった。 -- ファイル単位でマスクされる上限(1 つ)は、ワークスペース内だけでなく Codex の設定全体で数えられていた。 -- 現状でファイル単位にマスクされるのは `.env`、`~/.codex/auth.json`、`~/.claude/.credentials.json` の 3 つ(`~/.ssh` は存在せず、`**/*.pem` などに一致するファイルもない)。 - -### 判定: Codex CLI の不具合(設定ミスではない) - -- 公式ドキュメントは、完全パス(例: `~/.ssh`)とワイルドカード(例: `"**/*.env" = "deny"`)による deny を推奨している。現在の設定はこの使い方どおり。[7] -- openai/codex#43929 では、完全パスかワイルドカードかなどの条件によらず「deny に一致するファイルが 2 つ以上で必ず失敗、ディレクトリなら動く」と報告されている。0.155.1 でも未修正。[5] - -### 方針 - -- `.env` を守ることを最優先し、ファイル単位の 1 枠を `.env` に使う。 -- 認証ファイルはディレクトリ単位の deny に置き換える(#43929 によると、ディレクトリは上限に数えられない)。 - -| 対象 | 変更前 | 変更後 | -| ----------------------------- | ---------------------------------------- | ---------------------------------------------------------- | -| `.env` | `".env"` と `"**/.env"`(ファイル) | `".env"` のみ(同じファイルが 2 つと数えられるのを避ける) | -| `~/.claude/.credentials.json` | ファイル | `"~/.claude"`(ディレクトリ) | -| `~/.codex/auth.json` | ファイル | `"~/.codex"`(ディレクトリ) | -| `~/.ssh/**` | ワイルドカード(現在は何にも一致しない) | 変更なし | - -### リスクとフォールバック - -- `~/.codex` を deny すると、AGENTS.md を読み込むサンドボックスが `~/.codex` 配下(グローバルの AGENTS.md や skills)を読めず、失敗する可能性がある。 - - 失敗した場合は `"~/.codex"` の deny だけを外す。そうすると `~/.codex/auth.json` は Codex のコマンドから読めるようになるが、`.env` と `~/.claude` は守られたまま。 -- `"**/.env"` を外すため、サブディレクトリの `.env` は Codex の deny から外れる(現在は存在しない)。 -- `**/*.pem`、`**/*.key`、`**/secrets/**`、`**/config/credentials.json` に一致するファイルが今後 1 つでも置かれると、再び #43929 に当たる。 -- 次に #44304(bubblewrap 0.12.0 で `/proc` をマウントできないときの代替手段が働かない)に当たる可能性がある。[8] -- Claude 側の deny(`.claude/settings.json`)は変更しない。Claude にはこの制約がないため。 - -### 確認 - -- Codex CLI を再起動し、セッションが開始できること。 -- Codex から `.env`、`~/.claude/.credentials.json`、`~/.codex/auth.json` が読めず、`.env.example` が読めること。 - -## Phase 3d: `compose.yaml` に `systempaths=unconfined` を追加する(Codex の `/proc` 対策) - -### 経緯 - -- Phase 3c のあと、古い deny 設定を読み込んだまま動いていた VS Code 拡張の app server(PID 804、03:04 起動)を再起動した。すると Codex のエラーが `bwrap: Can't mount proc on /proc: Operation not permitted` に変わった。 -- deny の変更は反映された。次の問題として、[8] の `/proc` の問題が表に出た。 - -### 原因(根拠) - -- Codex 公式の secure devcontainer は「bubblewrap を setuid で入れる」方式で、Docker 内で `bwrap --proc /proc` が拒否されたときは、Codex が `--proc` なしで再試行する設計になっている。[9] -- bubblewrap 0.12.0 では setuid 方式が使えず、エラー文言も `/newroot/proc` から `/proc` に変わった。Codex は `/newroot/proc` という文字列で失敗を判定しているため、再試行されない。[8][10] -- #44304 と #44329 はどちらも Open で、PR もない。 -- Codex 公式ドキュメント(Permissions)には、コンテナ内での `/proc` の扱いについての記述がない。[7] - -### 変更 - -- `compose.yaml` の `web.security_opt` に `systempaths=unconfined` を追加する。 -- Docker が `/proc` の一部をマスクするのをやめるので、user namespace の中でも bwrap が新しい `/proc` をマウントできるようにする狙い(未検証)。 -- コンテナの作り直しが必要(Rebuild Container)。 - -### トレードオフ - -- Docker 公式の説明は "Turn off confinement for system paths (masked paths, read-only paths) for the container"。[11] 影響は次の 2 つで、コンテナ内の全プロセスが対象になる。 - - マスクの解除: Docker が隠していた `/proc` のパス(`/proc/kcore` など)が見えるようになる。 - - 読み取り専用の解除: `/proc/sys`、`/proc/sysrq-trigger` などが書き込めるようになる。こちらのほうが影響が大きい。 -- このコンテナは user namespace で root を分離していない(userns-remap なし)。そのため、コンテナ内で root(`sudo`)になると、ホストのカーネル設定を変えたり、`/proc/sysrq-trigger` でホストを再起動したりできる経路が開く。 - -#### 実害の範囲(ローカルの OrbStack で動かす場合) - -- ここでの「ホストのカーネル」は、macOS ではなく OrbStack が動かしている Linux VM のカーネル。影響が及ぶのはこの VM と、同じ VM で動くほかのコンテナまでで、Mac 本体のファイルには届かない。 -- 悪意のあるコードがコンテナ内で root になれた場合に、新たにできるようになること: - -| できるようになること | 実害 | -| -------------------------------- | -------------------------------------------------------------------------------------- | -| `/proc/sysrq-trigger` に書き込む | VM を即座に再起動・停止できる。OrbStack の全コンテナが止まる | -| `/proc/sys` に書き込む | VM のカーネル設定を変えられる。ほかのコンテナの保護を弱めることもできる | -| `/proc/kcore` を読む | VM のカーネルメモリを読める。ほかのコンテナの秘密情報(DB のパスワードなど)が漏れうる | - -- root でなくても、`/proc/timer_list` などから VM 内のプロセスの情報が少し見えるようになる(軽微)。 - -#### 誰がこれをできるか - -- devcontainer の `node` ユーザーは、通常パスワードなしで `sudo` を使える。`pnpm install` で入ったパッケージの postinstall スクリプトなど、サンドボックスの外で動くコードが悪意を持っていれば、root になれる。 -- Claude / Codex がサンドボックスの中で実行するコマンドは、bwrap が `sudo` による昇格を防ぐため、この経路は通らない。 - -#### 判断 - -- このコンテナにはすでに `SYS_ADMIN` と `seccomp` / `apparmor` の unconfined が与えられており、root ならこれらの保護を自分で外せる。`systempaths=unconfined` で新しくできるようになることはほとんどなく、変わるのは攻撃に必要な手間が 1 段減ること。 -- ただし「すでに緩めてあるから、さらに緩めてもよい」という理屈は、緩和を重ねる理由にはならない点に注意する。 -- ローカルの開発環境で、同じ OrbStack の VM に重要なコンテナを同居させない前提で許容する。この前提と実害の範囲は、チームに共有する。 -- Phase 2 の「却下した代替案」では Claude 用として退けたが、Codex には代わりの手段がないため採用する。Claude の `enableWeakerNestedSandbox` は、効果を確認できるまで残す。 - -### フォールバック - -- それでも `/proc` のエラーが出る場合は、この変更を戻し、上流の修正を待つ。 -- 上流の修正を待つ間は、`default_permissions` を外して組み込みの `:workspace` で動かす(`.env` が Codex から読めるようになる)かどうかを、チームで判断する。 - -### 確認 - -- Rebuild Container のあと、Codex CLI と VS Code 拡張の両方で、セッションが開始できること。 -- Codex から `.env`、`~/.claude/.credentials.json`、`~/.codex/auth.json` が読めず、`.env.example` が読めること。 -- Claude の Bash が引き続き動くこと。 - -## Phase 3e: コンテナの pnpm を `packageManager` に揃える - -### 経緯 - -- Codex が動いたあと、Claude CLI から commit できなかった。lefthook の `format` ジョブ(`pnpm exec prettier`)がサンドボックス内で `create the temporary package manager install directory: Read-only file system` で失敗していた。 -- `pnpm test:unit` や `pnpm lint` など、Claude がサンドボックス内で実行する `pnpm` はすべて同じ理由で失敗する。 - -### 原因 - -- pnpm 11 以降は、実行中の pnpm と `package.json` の `packageManager` のバージョンが違うと、指定のバージョンを自動でダウンロードして使う(`pmOnFail: download` が既定)。[12] -- コンテナの pnpm はベースイメージ(`mcr.microsoft.com/devcontainers/javascript-node:24`)に入っている 12.3.4。`packageManager` は Renovate が上げていて 12.4.2。 -- サンドボックスの中ではダウンロード先に書き込めないため失敗する。サンドボックスの外では黙ってダウンロードして動くため、これまで気づかなかった。 -- `Dockerfile` と `setup-devcontainer.sh` は、過去に一度も pnpm のバージョンを固定していない(git 履歴で確認)。 - -| 日付 | `packageManager` | コンテナの 12.3.4 との関係 | -| ----- | ---------------- | -------------------------- | -| 09-05 | pnpm@12.3.4 | 一致 | -| 09-13 | pnpm@12.4.1 | ここからずれた | -| 09-18 | pnpm@12.4.2 | ずれたまま | - -- ずれ始めた 09-13 は、#4033 / #4034 で Claude の Bash が全滅した時期と重なる。そのため、サンドボックスを直すまで表に出なかった。 -- 09-01 以前(pnpm 11 の時期)に問題にならなかった理由は、当時のイメージの pnpm の版が分からず未確認。 - -### 変更(案 A) - -- `.devcontainer/setup-devcontainer.sh` の `pnpm install` の直前で、`package.json` の `packageManager` と同じ版の pnpm をグローバルに入れる。 -- バージョンを 2 か所に書かないので、Renovate が `packageManager` を上げても、次のリビルドで自動的に追従する。 - -```bash -# Match the global pnpm to `packageManager`; a mismatch makes pnpm download the pinned version, -# which the agent sandboxes cannot write, so every sandboxed `pnpm` command fails. -pnpm_version="$(node -p "require('./package.json').packageManager.split('@')[1].split('+')[0]")" -npm install -g "pnpm@${pnpm_version}" -``` - -- `+sha512...` のハッシュが `packageManager` に付いた場合に備え、`+` 以降は取り除く。 -- 失敗したときは、後続の `pnpm install` と同じくセットアップ全体を失敗させる(黙ってずれたままにしない)。 - -### 却下した代替案 - -| 案 | 却下理由 | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| サンドボックスで pnpm のキャッシュへの書き込みを許可(`allowWrite`) | サンドボックスを緩める。書き込めるようになった pnpm 本体を書き換えられる余地が生まれる | -| `Dockerfile` に pnpm の版を直接書く | `package.json` と 2 か所で管理することになり、Renovate の更新でまたずれる | -| `pmOnFail: ignore` にしてダウンロードさせない | `packageManager` による版の固定が効かなくなる | - -### 確認 - -- Rebuild Container のあと、`pnpm --version` が `packageManager` と一致すること。 -- Claude CLI から `pnpm exec prettier --version` と `git commit` が通ること。 -- 今すぐの回避(リビルド前): ご自身のターミナルで `npm install -g pnpm@12.4.2`。 - -## Phase 4: リビルドと検証(高リスク・開発環境全体に影響) - -- devcontainer を Rebuild Container する。 -- Claude / Codex の両方で次を確認する。 - - `git commit` まで通ること - - `.env`、`~/.codex/auth.json`、`~/.claude/.credentials.json` が読めないこと -- `pnpm format`、`pnpm lint`、`git diff --check` を実行する。 - -## レビュー - -- 秘密情報の扱いの変更(deny 設定)を含むため、AGENTS.md に従いクロスレビューの対象とする。 -- Claude 主導の変更なので Codex でレビューする。Codex が使えない場合は `coderabbit review --plain` を使う。 - -## 出典 - -1. bubblewrap releases(0.11.2 で setuid を非推奨化、0.12.0 で削除): https://github.com/containers/bubblewrap/releases -2. bubblewrap source(`acquire_privs()` の `die ("setuid use of bubblewrap is not supported")`): https://github.com/containers/bubblewrap/blob/main/bubblewrap.c -3. Debian security tracker(DSA-6472-1 / CVE-2026-87766、trixie で 0.12.0-1~deb13u1): https://security-tracker.debian.org/tracker/DSA-6472-1 / https://security-tracker.debian.org/tracker/CVE-2026-87766 -4. Claude Code sandboxing(Troubleshooting「Bubblewrap fails to start inside a container」、Security limitations): https://code.claude.com/docs/en/sandboxing -5. openai/codex#43929(deny ルールに一致するファイルが 2 つ以上あると起動に失敗): https://github.com/openai/codex/issues/43929 -6. Claude Code settings reference(`sandbox.enableWeakerNestedSandbox`、Scope: Any file): https://code.claude.com/docs/en/settings-reference -7. Codex Permissions(deny の完全パスとワイルドカード、`:workspace_roots`、`glob_scan_max_depth`): https://learn.chatgpt.com/docs/permissions -8. openai/codex#44304(bubblewrap 0.12.0 で `/proc` のエラー文言が変わり、代替手段が働かない): https://github.com/openai/codex/issues/44304 -9. openai/codex PR #17547(secure devcontainer で bubblewrap を setuid で入れる。`/proc` のマウントが拒否されたら `--proc` なしで再試行): https://github.com/openai/codex/pull/17547 -10. openai/codex#44329(CLI と VS Code 拡張が `/proc` のマウント失敗を扱えない): https://github.com/openai/codex/issues/44329 -11. Docker `docker container run`(`--security-opt systempaths=unconfined`): https://docs.docker.com/reference/cli/docker/container/run/ -12. pnpm 11.0 release notes(`pmOnFail`、既定は `download`): https://pnpm.io/blog/releases/11.0 diff --git a/docs/dev-notes/2026-09-18/fix-bwrap-setuid/summary.md b/docs/dev-notes/2026-09-18/fix-bwrap-setuid/summary.md new file mode 100644 index 000000000..8b479e22f --- /dev/null +++ b/docs/dev-notes/2026-09-18/fix-bwrap-setuid/summary.md @@ -0,0 +1,141 @@ +# devcontainer 内で Claude / Codex のサンドボックスが全滅した件 + +Issue: https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/4059 +Commit: 40450ec7(旧 `plan.md` / `survey.md` / `learning.md` を本ファイルに統合。原文は同コミットで参照可) + +## 症状と発端 + +- Claude / Codex の Bash がすべて `bwrap: setuid use of bubblewrap is not supported` で失敗し、`git commit` もできない。 +- 発端: DSA-6472-1(2026-08-27、CVE-2026-87766 修正)で Debian trixie に bubblewrap 0.12.0 が入った。0.12.0 は setuid 対応を削除しており、`acquire_privs()` が実 UID ≠ 実効 UID を検知して即終了する。[1][2][3] +- `Dockerfile` は `apt-get install bubblewrap` で版を固定していないため、再ビルドした日によって 0.11.x(setuid で動く)か 0.12.0 かが変わる。実環境で `bwrap --version` = 0.12.0、Debian 13.6 を確認(2026-09-19)。 +- `allowUnsandboxedCommands: false`(Strict sandbox mode)なので、bwrap が起動できないとサンドボックス外で再実行する逃げ道がなく、Bash が全滅する。[4] +- #4034(2026-09-13 マージ)の時点で動いていたのは、DSA 配信前にビルドしたイメージ/キャッシュが残っていたためと推測(未確認)。 + +## 障害の連鎖と対応する修正 + +setuid の削除で、それまで先頭の失敗に隠れていた問題が 1 つずつ表に出た。 + +| # | 表に出たエラー | 原因 | 修正(40450ec7) | +| --- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| 1 | `setuid use of bubblewrap is not supported` | 0.12.0 が setuid を拒否 | `Dockerfile` の `chmod u+s /usr/bin/bwrap` を削除 | +| 2 | Claude: `Can't mount proc on /proc: Operation not permitted` | setuid が暗黙に回避していた `/proc` マウント制約(下記) | `.claude/settings.json` に `enableWeakerNestedSandbox: true` | +| 3 | Codex: `Can't write data to file : Bad file descriptor` | openai/codex#43929(ファイル単位の deny が 2 つ以上で起動失敗) | 認証情報の deny をディレクトリ単位へ。ファイル単位は `.env` の 1 つだけ | +| 4 | Codex: `Can't mount proc on /proc` | #2 と同じ制約 + Codex の代替手段が新しいエラー文言を検知しない(#44304 / #44329) | `compose.yaml` に `security_opt: systempaths=unconfined` | +| 5 | サンドボックス内の `pnpm` が `Read-only file system` | コンテナの pnpm と `packageManager` の版ずれ → pnpm が指定版をダウンロードしようとする | `setup-devcontainer.sh` で `packageManager` と同じ版を入れる | + +### #2 / #4: setuid が回避していた `/proc` の制約 + +- user namespace 方式では、既存の `/proc` がすべて見えている場合にしかカーネルは新しい procfs のマウントを許さない。Docker は `/proc/kcore` などをマスクしているので EPERM になる。 +- setuid 方式ではコンテナの root(`SYS_ADMIN`)で動くため、この制約を受けなかった。つまり setuid は「コンテナ内で `/proc` をマウントする」回避策を兼ねていた。 +- Claude: 公式 Troubleshooting の対処そのもの("the inner sandbox bind-mounts the container's existing `/proc` instead")。注意書き "considerably weakens security and should only be used when additional isolation is otherwise enforced" は、コンテナが外側の隔離境界になるので許容。[4][6] +- Codex: 公式 secure devcontainer は setuid 前提で、`--proc` が拒否されたら `--proc` なしで再試行する設計。[9] だが失敗判定が旧文言 `/newroot/proc` の文字列一致なので、0.12.0 では再試行されない。[8][10] Codex 側に設定で逃げる手段がないため、Docker のマスク自体を外した。 +- `enableWeakerNestedSandbox` は `systempaths=unconfined` 導入前に入れたもので、「効果を確認できるまで残す」としている。現状では不要かもしれない(未検証)。 + +### #3: openai/codex#43929 の実際の数え方 + +- 「deny に一致するファイルが 2 つ以上で必ず失敗、ディレクトリなら動く」。完全パスかワイルドカードかによらない。0.155.1 でも未修正。[5] +- 実測で分かったこと: + - 数える範囲はワークスペース内ではなく Codex の設定全体(`~/.claude/.credentials.json` で失敗した)。 + - ワイルドカードを含まない完全パス(例 `".env.local"`)は、実在しなくても 1 つに数えられる。 + - ワイルドカードのパターンは実在ファイルにのみ一致すると考えられる(未検証)。 + - `".env"` と `"**/.env"` を同じファイルとして 2 回数えるかは未確認。安全側で `".env"` だけにした。 +- 設定は公式ドキュメント推奨の書き方どおりで、設定ミスではなく Codex のバグ。[7] + +### #5: pnpm の版ずれ + +- pnpm 11 以降は `pmOnFail: download` が既定で、版がずれると指定版を自動ダウンロードする。[12] サンドボックス外では黙って成功するため気づかなかった。 +- ベースイメージの pnpm は 12.3.4、`packageManager` は Renovate により 09-13 に 12.4.1、09-18 に 12.4.2。ずれ始めた 09-13 は Bash 全滅の時期と重なり、サンドボックス復旧まで表に出なかった。 +- 版を `package.json` から読むので、Renovate が上げても次のリビルドで追従する(2 か所管理にしない)。 + +## 意思決定 + +### 守るもの(固定) + +- `.env`、`~/.claude/.credentials.json`、`~/.codex/auth.json` を Claude / Codex の両方から読めないこと。これを削る案(`.env` の deny 解除、`default_permissions` を外す等)は採らない。 + +### deny 設定の最終形とトレードオフ + +- Codex: `"~/.codex"` / `"~/.claude"` をディレクトリで deny、ファイル単位は `".env"` のみ。`**/.env` と `.env.*` 系は外した。 +- Claude: `**/.env.*` 系を外し `**/.env` のみ(Codex と集合を揃えるため)。Claude 自体には #43929 の制約はない。 +- 失うもの: + - サブディレクトリの `.env` と、`.env.local` などは deny から外れる(現状は存在しない)。置くときは deny を足し直す必要があり、#43929 が未修正なら Codex が再び起動しなくなる。 + - `**/*.pem`、`**/*.key`、`**/secrets/**`、`**/config/credentials.json` に一致するファイルが 1 つでも置かれると #43929 に当たる。 + - `~/.codex` の deny で、サンドボックス内から `~/.codex` 配下(グローバル AGENTS.md、skills)が読めなくなる可能性。問題が出たら `"~/.codex"` だけ外す。 + +### `systempaths=unconfined` のリスク評価 + +- Docker の説明は "Turn off confinement for system paths (masked paths, read-only paths)"。[11] コンテナ内の全プロセスで、`/proc/kcore` 等のマスクと `/proc/sys`、`/proc/sysrq-trigger` の読み取り専用が外れる。 +- userns-remap がないため、コンテナ内 root は VM カーネルを直接操作できる。 + +| 新たにできること(root の場合) | 実害 | +| -------------------------------- | ----------------------------------------------------------- | +| `/proc/sysrq-trigger` に書き込む | VM を即座に再起動・停止。OrbStack の全コンテナが止まる | +| `/proc/sys` に書き込む | VM のカーネル設定変更。他コンテナの保護も弱められる | +| `/proc/kcore` を読む | VM のカーネルメモリ読み出し。他コンテナの秘密情報が漏れうる | + +- 範囲: OrbStack の Linux VM とその中のコンテナまで。Mac 本体のファイルには届かない。 +- 誰が: `node` はパスワードなし `sudo` が使えるので、サンドボックス外で動くコード(悪意ある postinstall など)。サンドボックス内のコマンドは bwrap が `sudo` 昇格を防ぐので通らない。 +- 判断: 既存の `SYS_ADMIN` / seccomp・apparmor unconfined で root は元々これらを外せるため、新たな能力はほぼ増えず攻撃の手間が 1 段減るのみ。ただしこれは緩和を重ねる理由ではないので、単体で実害を評価した上で「同じ VM に重要なコンテナを同居させない」前提で許容する。この前提はチームに共有する。 +- フォールバック: それでも `/proc` エラーが出るなら戻して上流の修正を待つ。 + +### 却下した代替案 + +| 案 | 却下理由 | +| ----------------------------------------------------------- | ------------------------------------------------------------------------- | +| `allowUnsandboxedCommands: true` / `sandbox.enabled: false` | 原因を直さず防御を外すだけ。`denyRead` による認証情報の保護も効かなくなる | +| bubblewrap を 0.11.x に固定 | 廃止済みの setuid 方式の延命。CVE-2026-87766 の修正も受けられない [3] | +| `.env` を deny から外して Codex を動かす | 守るものを削る本末転倒 | +| Codex の deny を残して上流の修正を待つ | 修正の目処がなく、その間 Codex が起動しない | +| サンドボックスに pnpm キャッシュへの書き込みを許可 | サンドボックスを緩め、pnpm 本体を書き換えられる余地が生まれる | +| `Dockerfile` に pnpm の版を直書き | `package.json` と 2 か所管理になり、Renovate の更新でまたずれる | +| `pmOnFail: ignore` | `packageManager` による版の固定が効かなくなる | + +## 運用上の注意 + +- Codex の設定を変えたら、VS Code 拡張が起動した app server も再起動する。CLI は常駐 app server に接続するだけで設定を読み直さない(今回、03:04 起動の古い app server が古い deny のまま動いていた)。 +- 上流の修正(#43929、#44304 / #44329)が入ったら、ディレクトリ単位の deny、`systempaths=unconfined`、`enableWeakerNestedSandbox` の要否を見直す。 + +## 振り返り + +### 紆余曲折の根本原因 + +修正は 1 コミットだが、到達までに Phase 3 → 3b → 3c → 3d → 3e と 5 回方針を出し直した。原因は次の 3 つに集約される。 + +1. setuid が暗黙に担っていた役割を洗い出さずに外した。 + - setuid が `/proc` マウントの回避策を兼ねていたことを見落とし、Claude / Codex の `/proc` 問題を別々に扱った。そのため `systempaths=unconfined` を Claude 用に却下 → Codex 用に採用という往復が起きた。#44304 は Phase 3c の時点で「次に当たる可能性」と把握していたのに先送りした。 +2. 一次資料を読み切らず、観測したエラーから仮説を狭く立てた。 + - #43929 の条件(ファイル単位の deny が 2 つ以上で失敗、ディレクトリは可)を「ワークスペース内の実在ファイルが 2 つ以上」と読み替えたため、`.env.example` → `.env.local` → `~/.claude/.credentials.json` と 3 回に分けて潰すことになった。 +3. 検証環境が変更を反映しているか確認しなかった。 + - 「CLI を再起動すれば反映される」と説明し、常駐 app server が古い設定のまま出したエラーで判断した回があった。 + +背景として、先頭の失敗が後ろの問題をすべて隠す構造(Strict sandbox mode で Bash が全滅 → Codex のバグと pnpm の版ずれが見えない)があり、1 つ直すたびに次の層が現れた。これ自体は避けられないが、上の 1〜3 がなければ往復は 1〜2 回で済んだ。 + +### 判断の誤り(結果には残っていないもの) + +- `.env` を deny から外す案を推した: 動かすことを優先して目的(秘密情報の保護)を損ねる本末転倒。 +- 「すでに緩めてあるから」を理由に `systempaths=unconfined` のリスクを小さく見積もった: 既存の緩和は追加の緩和の根拠にならない。 + +### 教訓 + +- 権限まわりの設定(setuid、capability、security_opt)を外すときは、それが暗黙に回避していた制約を先に洗い出す。 +- エラーが連鎖しそうなときは、既知の上流 issue をまとめて確認し、方針を 1 回で決める。 +- 上流 issue を根拠にするときは、報告された条件をそのまま採用し、自分の観測で狭めない。 +- 設定変更が効かないときは、その設定を読むのが常駐プロセスかを確かめ、プロセスの起動時刻と設定の更新時刻を比べる。 +- バージョン未固定のパッケージは、再ビルドした日によって挙動が変わる。「以前は動いた」ときは、まず実環境の版を確認する(`bwrap --version`)。 +- 回避策を選ぶ前に守るもの(今回は `.env` と認証情報)を固定し、それを削る案は候補から外す。 +- セキュリティの緩和は、既存の緩和を根拠にせず、単体で実害(誰が・何を・どこまで)を書き出して判断する。 + +## 出典 + +1. bubblewrap releases(0.11.2 で setuid 非推奨化・CVE-2026-41163、0.12.0 で削除): https://github.com/containers/bubblewrap/releases +2. bubblewrap source(`acquire_privs()` の `die ("setuid use of bubblewrap is not supported")`): https://github.com/containers/bubblewrap/blob/main/bubblewrap.c +3. Debian security tracker: https://security-tracker.debian.org/tracker/DSA-6472-1 / https://security-tracker.debian.org/tracker/CVE-2026-87766 / https://security-tracker.debian.org/tracker/CVE-2026-41163 +4. Claude Code sandboxing(Troubleshooting「Bubblewrap fails to start inside a container」、Strict sandbox mode): https://code.claude.com/docs/en/sandboxing +5. openai/codex#43929(deny に一致するファイルが 2 つ以上で起動失敗): https://github.com/openai/codex/issues/43929 +6. Claude Code settings reference(`sandbox.enableWeakerNestedSandbox`): https://code.claude.com/docs/en/settings-reference +7. Codex Permissions(deny の完全パスとワイルドカード、`:workspace_roots`): https://learn.chatgpt.com/docs/permissions +8. openai/codex#44304(0.12.0 で `/proc` のエラー文言が変わり代替手段が働かない): https://github.com/openai/codex/issues/44304 +9. openai/codex PR #17547(secure devcontainer、`--proc` なしで再試行): https://github.com/openai/codex/pull/17547 +10. openai/codex#44329(CLI と VS Code 拡張が `/proc` マウント失敗を扱えない): https://github.com/openai/codex/issues/44329 +11. Docker `docker container run`(`--security-opt systempaths=unconfined`): https://docs.docker.com/reference/cli/docker/container/run/ +12. pnpm 11.0 release notes(`pmOnFail`): https://pnpm.io/blog/releases/11.0 diff --git a/docs/dev-notes/2026-09-18/fix-bwrap-setuid/survey.md b/docs/dev-notes/2026-09-18/fix-bwrap-setuid/survey.md deleted file mode 100644 index a007ed991..000000000 --- a/docs/dev-notes/2026-09-18/fix-bwrap-setuid/survey.md +++ /dev/null @@ -1,141 +0,0 @@ -# Survey: #4033 以降に Claude / Codex から commit できない問題 - -Issue: https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/4059 - -## 症状 - -- Claude Code / Codex 経由の Bash コマンドがすべて `bwrap: setuid use of bubblewrap is not supported` で失敗する。 -- `git commit` だけでなく `gh`・`ls` なども含め、Bash が一切動かない。 - -## 結論 - -- Debian stable のセキュリティ更新で bubblewrap 0.12.0 が入り、setuid で bwrap を動かす方式が上流で廃止されていた。 -- `Dockerfile` の `chmod u+s /usr/bin/bwrap` がこの廃止済みの使い方に該当し、コンテナの再ビルド後に bwrap が起動直後に終了するようになった。 -- 設定ミスというより、上流の方針変更とバージョン未固定の `apt-get install` が重なったことが原因。 - -```dockerfile -RUN apt-get update \ - && apt-get -y install --no-install-recommends bubblewrap fish \ - && chmod u+s /usr/bin/bwrap \ - && rm -rf /var/lib/apt/lists/* -``` - -## 根本原因 - -### 1. bwrap の 2 つの動作方式 - -| 方式 | 仕組み | 現状 | -| ------------------- | ---------------------------------------------------------------------------- | ---------- | -| user namespace 方式 | 一般ユーザーのままカーネルの unprivileged user namespace で namespace を作る | 現在の標準 | -| setuid 方式 | `chmod u+s` で root 権限付きで起動し、その権限で namespace を作る | 廃止 | - -- Claude Code 公式ドキュメントも bubblewrap を "the unprivileged sandboxing tool" と説明している。 -- setuid 方式は user namespace が使えない古いカーネル向けの後方互換手段だった。 - -### 2. 上流 bubblewrap での廃止(公式リリースノート) - -- 0.11.2(2026-04): CVE-2026-41163(setuid で入れた bwrap に ptrace で割り込み、サンドボックス準備処理を乗っ取れる)を受けて setuid を非推奨化した。 - - 新しいビルドオプション `-Dsupport_setuid` の既定値は false で、"Binaries built with this will refuse to run if made setuid." -- 0.12.0(2026-08-26): setuid 対応を完全に削除した。 - - "This version removes the support for building a setuid bubblewrap. ... basically all modern linux distributions now support unprivileged user namespaces" - -今回のエラーは `bubblewrap.c` の `acquire_privs()` から出ている。 - -```c -/* Are we setuid ? */ -if (real_uid != euid) - { - /* Historically we supported this, but now we only do user namespaces */ - die ("setuid use of bubblewrap is not supported"); - } -``` - -- setuid ビットが付いていると実 UID(node)と実効 UID(root)が食い違い、bwrap はそれを検知して即終了する。 - -### 3. 私たちの環境に入った経路(Debian security tracker) - -- 2026-08-27: DSA-6472-1 で trixie-security に `bubblewrap 0.12.0-1~deb13u1` が配信された。 - - 修正対象は CVE-2026-87766(サンドボックス準備中に親ディレクトリのシンボリックリンクをたどり、ホスト側へ書き込めてしまう)。 - - Debian は stable にもかかわらずメジャー更新の 0.12.0 をそのまま入れた(bookworm には修正が大がかりすぎるとして backport されていない)。 -- `Dockerfile` は `apt-get install bubblewrap` でバージョンを固定していない。 - - そのため、ビルドした日によって 0.11.x(setuid で動く)と 0.12.0(setuid だと即終了)のどちらが入るかが変わる。 - -### 4. Bash が全滅する理由(Claude Code 公式ドキュメント) - -- Linux では Bash コマンドを 1 つずつ bwrap で包んで実行する。 -- `.claude/settings.json` は `allowUnsandboxedCommands: false`(公式ドキュメント上は Strict sandbox mode)。 - - この設定では "every command Claude runs must run sandboxed" となり、サンドボックスの外で再実行する逃げ道がない。 -- よって bwrap の起動自体が失敗すると、個別コマンドの許可設定とは無関係にすべての Bash が失敗する。 -- Codex も同じ `/usr/bin/bwrap` を使うため、両方同時に影響を受ける。 - -### 5. setuid が実際に回避していたもの(当初の「setuid は不要」は誤り) - -- setuid を外すと、Claude の Bash は `bwrap: Can't mount proc on /proc: Operation not permitted` で失敗するようになった。 -- user namespace 方式では、カーネルは既存の `/proc` がすべて見えている場合にしか新しい procfs のマウントを許さない。 -- Docker は `/proc/kcore` などをマスク(上書きマウント)しているため、この条件を満たせず EPERM になる。 -- setuid 方式では bwrap がコンテナの root 権限(`SYS_ADMIN`)で動くため、この制約を受けずに `/proc` をマウントできていた。 -- Claude Code 公式ドキュメントの Troubleshooting にも同じ症状が載っている: "in an unprivileged container, bubblewrap can't mount a fresh `/proc` filesystem ... Set `enableWeakerNestedSandbox` to `true` so the inner sandbox bind-mounts the container's existing `/proc` instead." -- つまり setuid は「コンテナ内で `/proc` をマウントする」ための回避策として機能していた。 - -## 現環境での確認結果(2026-09-19) - -```bash -$ cat /etc/debian_version; bwrap --version -13.6 -bubblewrap 0.12.0 -``` - -- Debian 13(trixie)上で bubblewrap 0.12.0 が入っていることを確認した。上記の根本原因と一致する。 - -## #4034 時点で動いていた理由(推測) - -- #4034 のマージは 2026-09-13 で、DSA の配信(2026-08-27)より後。 -- PR の作業中(8/27 より前)にビルドしたイメージ、またはビルドキャッシュを使っていたため、setuid 対応の 0.11.x が残っていたと推測している。 - -## setuid を外した後に出た 2 つのエラー - -setuid を外すと、Claude と Codex でそれぞれ別の問題が表に出た。 - -### Claude: `Can't mount proc on /proc: Operation not permitted` - -- 原因は上記 5。コンテナ内で新しい `/proc` をマウントできない。 -- 案 A(公式ドキュメントの対処): `.claude/settings.json` に `sandbox.enableWeakerNestedSandbox: true` を追加する。 - - bwrap は新しい `/proc` をマウントせず、コンテナの `/proc` を bind mount する。 - - 公式ドキュメントの注意書き: "considerably weakens security and should only be used when additional isolation is otherwise enforced." この環境ではコンテナ自体が外側の隔離境界になる。 -- 案 B: `compose.yaml` の `security_opt` に `systempaths=unconfined` を追加し、Docker による `/proc` のマスクをやめる。 - - Claude のサンドボックスの強度は保てるが、コンテナ内の全プロセスからマスク対象だった `/proc` のパスが見えるようになる。 - -### Codex: `Can't write data to file /usr/src/app/.env.example: Bad file descriptor` - -- Codex 側の既知の未修正バグ openai/codex#43929: ワークスペース直下で deny ルールにマッチするファイルが 2 つ以上あると、Codex が起動時に失敗する(0.152.1 / 0.153.4 で再現が報告されている)。 -- `.codex/config.toml` の `.env` / `.env.*` に、`.env` と `.env.example` の 2 ファイルがマッチしている。 -- bwrap の setuid 問題とは別件で、setuid の問題で起動前に止まっていたため今まで見えていなかったと推測している(#4034 の時点で動いていた理由は未確認)。 -- 対処候補: 上流の修正を待つか、deny にマッチする実ファイルを 1 つ以下に絞る(例: 秘密情報を含まないテンプレートの `.env.example` を deny 対象から外す)。 - -## 修正方針 - -- `Dockerfile` から `&& chmod u+s /usr/bin/bwrap \` の 1 行を削除する(bubblewrap 0.12.0 では setuid 方式自体が使えないため必須)。 -- Claude: 案 A または案 B で `/proc` の問題に対処する。 -- Codex: openai/codex#43929 への対処を決める。 - -## 却下した代替案 - -- `allowUnsandboxedCommands: true` にする: サンドボックスの外での実行を許すことになり、`denyRead` による認証情報の保護が弱まる。 -- `sandbox.enabled: false` にする: 同上。原因を直さずに防御を外すだけになる。 -- bubblewrap を 0.11.x に固定する: 廃止済みの setuid 方式を延命するだけで、CVE-2026-87766 の修正も受けられなくなる。 - -## 未確認事項 - -- 修正後に Claude / Codex の両方で `git commit` が通ることを確認する必要がある。 - -## 参考 - -- bubblewrap releases: https://github.com/containers/bubblewrap/releases -- bubblewrap source (`acquire_privs`): https://github.com/containers/bubblewrap/blob/main/bubblewrap.c -- Debian tracker: https://tracker.debian.org/pkg/bubblewrap -- DSA-6472-1: https://security-tracker.debian.org/tracker/DSA-6472-1 -- CVE-2026-41163: https://security-tracker.debian.org/tracker/CVE-2026-41163 -- CVE-2026-87766: https://security-tracker.debian.org/tracker/CVE-2026-87766 -- Claude Code sandboxing: https://code.claude.com/docs/en/sandboxing -- Claude Code settings reference(`sandbox.enableWeakerNestedSandbox`): https://code.claude.com/docs/en/settings-reference -- openai/codex#43929: https://github.com/openai/codex/issues/43929 diff --git a/docs/dev-notes/2026-09-19/fix-bwrap-setuid/learning.md b/docs/dev-notes/2026-09-19/fix-bwrap-setuid/learning.md deleted file mode 100644 index 9860d0706..000000000 --- a/docs/dev-notes/2026-09-19/fix-bwrap-setuid/learning.md +++ /dev/null @@ -1,29 +0,0 @@ -# Learning: devcontainer 内で Claude / Codex のサンドボックスが全滅した件 - -## 問題 - -- 症状: devcontainer 内で Claude / Codex の Bash が全滅し、`git commit` もできない(`bwrap: setuid use of bubblewrap is not supported` → `/proc` マウント失敗 → Codex の `Bad file descriptor`)。 -- 根本原因: Debian のセキュリティ更新で入った bubblewrap 0.12.0 が setuid 方式を廃止し、setuid が暗黙に担っていた `/proc` マウントの回避策が消えた。その結果、Codex 側の未修正バグ 2 件(ファイル単位の deny が 2 つ以上で失敗、`/proc` 失敗時の代替手段が新しいエラー文言を検知できない)が順に表に出た。 - -## 有効だったアプローチ - -- エラー文言で上流のソース(`bubblewrap.c` の `acquire_privs()`)とリリースノート、Debian security tracker を引き、「いつ・なぜ挙動が変わったか」を一次情報で確定させた。 -- エラーの対象パスが変わるたびに「どの設定行が効いているか」を実ファイル(`ls`、`find`)と突き合わせ、仮説を 1 つずつ潰した。 -- 設定が反映されないときは、常駐プロセスの起動時刻と設定ファイルの更新時刻を比べた(VS Code 拡張が起動した Codex の app server が、古い設定のまま CLI の接続先になっていた)。 -- Claude は公式ドキュメントの `enableWeakerNestedSandbox`、Codex は `systempaths=unconfined` で `/proc` のマウントを通し、実害の範囲(OrbStack の VM 内に閉じる)を明記して許容した。 - -## ハマった点 - -- 「setuid は不要」と判断した: setuid が `/proc` マウントの回避策を兼ねていたことを見落としていた。 -- 「ワークスペース内の実在ファイルが 2 つ以上で失敗」と狭く理解した: 実際は、実在しない完全パスも含めて、Codex の設定全体で数えられていた。そのため `.env.example` → `.env.local` → `~/.claude/.credentials.json` と、3 回に分けて潰すことになった。 -- 「CLI を再起動すれば反映される」と説明した: CLI は常駐している app server に接続するだけで、設定を読み直していなかった。 -- `.env` を deny から外す案を推した: 「動かすこと」を優先して目的(秘密情報の保護)を損ねる本末転倒だった。 -- 「すでに緩めてあるから」を理由に `systempaths=unconfined` のリスクを小さく見積もった: 緩和を重ねる理由にはならない。 - -## 教訓 - -- 権限まわりの設定(setuid、capability、security_opt)を外すときは、それが暗黙に回避していた制約を先に洗い出す。例: setuid が `/proc` マウントの制約を回避していた。 -- バージョンを固定しないパッケージは、再ビルドした日によって挙動が変わる。「以前は動いた」ときは、まず実環境のバージョンを確認する。例: `bwrap --version` で 0.12.0 を確認。 -- 設定の変更が効かないときは、その設定を読むのが常駐プロセスかどうかを確かめ、プロセスの起動時刻を設定の更新時刻と比べる。 -- 回避策を選ぶときは、守りたいもの(今回は `.env` と認証情報)を先に固定し、それを削る案は候補から外す。 -- セキュリティの緩和は、既存の緩和を根拠にせず、単体で実害(誰が・何を・どこまで)を書き出してから判断する。