Podman上のCodex CLIでapply_patchがbwrapエラーになる問題を解決した話
Podman上のCodex CLIでapply_patchがbwrapエラーになる問題を解決した話
Windows上のPodmanを使ったdevcontainer環境でCodex CLIを動かしていたところ、apply_patchを使用したファイル編集だけが失敗する問題に遭遇しました。
通常のファイル編集はできるため致命的ではなかったのですが、Codexが別の方法へフォールバックして編集している状態であり、毎回エラーが出るのも気になります。
調べていくと、原因はCodexそのものというより、CodexがLinux sandboxとして利用するBubblewrap(bwrap)と、Podmanコンテナに付与していたLinux capabilityの組み合わせでした。
さらに、いったん解決したあとCodex CLIが自動アップデートされたことで問題が再発するというオチまであったので、最終的な対策までまとめておきます。
環境
今回の環境は少し特殊で、ざっくり以下のような構成です。
Windows
└─ WSL2
└─ Podman
└─ devcontainer
└─ node user
└─ npm版 Codex CLI
devcontainer自体をCodex用のsandboxとして使っており、コンテナ内では通信制限のためにiptablesも設定しています。
そのため、devcontainer.jsonには以下の設定が入っていました。
"runArgs": [
"--cap-add=NET_ADMIN",
"--userns=keep-id"
]
NET_ADMINはiptablesによるfirewall設定のために必要です。
症状
Codex CLI自体は普通に動作し、ファイルの読み書きもできます。
しかし、Codexがapply_patchを使用しようとしたときだけbwrap関連のエラーが発生していました。
Codexは別の方法を使えばファイルを編集できるため作業そのものは継続できますが、sandbox周辺で何かがおかしいことは明らかです。
まずbwrap単体を調べる
コンテナ内の状態を確認しました。
codex --version
command -v bwrap
bwrap --version
id
cat /proc/self/uid_map
cat /proc/self/gid_map
grep -E 'NoNewPrivs|Seccomp|Cap(Eff|Bnd)' /proc/self/status
unshare -Ur true
echo "unshare rc=$?"
bwrap --unshare-user --ro-bind / / true
echo "bwrap-user rc=$?"
bwrap --unshare-user --unshare-net --ro-bind / / true
echo "bwrap-net rc=$?"
この時点では以下のような状態でした。
codex-cli 0.147.0
/usr/bin/bwrap
bubblewrap 0.8.0
uid=1000(node) gid=1000(node)
CapEff: 0000000000001000
NoNewPrivs: 0
Seccomp: 2
unshare rc=0
unshare -Urが成功しているため、Podmanコンテナ内からさらにuser namespaceを作れないことが原因ではありませんでした。
問題はbwrapを直接実行すると出てきました。
bwrap: Unexpected capabilities but not setuid, old file caps config?
bwrap-user rc=1
原因はNET_ADMINだった
ポイントはこの値です。
CapEff: 0000000000001000
0x1000はCAP_NET_ADMINです。
つまり、devcontainerに指定していた
--cap-add=NET_ADMIN
によって、Codexを実行しているnodeユーザーにもNET_ADMIN capabilityが残っていました。
一方、non-setuidのBubblewrapは、呼び出し元が予期しないcapabilityを保持している状態を拒否します。
構造としては以下のようになっていました。
Podman container
│
├─ CAP_NET_ADMIN
│
▼
node
│
▼
Codex CLI
│
▼
bwrap
│
└─ Unexpected capabilities...
ではNET_ADMINを消せばいいのか
単純なCodex用コンテナならそれでもよいのですが、今回の環境ではそうはいきません。
devcontainer起動時にiptablesを設定しているためです。
iptables -P INPUT DROP
iptables -P OUTPUT DROP
iptables -P FORWARD DROP
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT
この操作にはNET_ADMINが必要なので、devcontainerから単純に削除することはできません。
setuid bwrapも試したがPodmanではうまくいかなかった
次にBubblewrapをsetuid rootとして動作させる方法を試しました。
RUN chown root:root /usr/bin/bwrap \
&& chmod 4755 /usr/bin/bwrap
ところが、今度は以下のエラーになります。
bwrap: capset failed: Operation not permitted
SETUID、SETGIDを追加したり、seccompを無効化したりして切り分けましたが、結果は変わりませんでした。
CapEff: 00000000000010c0
NoNewPrivs: 0
Seccomp: 0
bwrap: capset failed: Operation not permitted
今回のようなrootless Podmanと--userns=keep-idを組み合わせた環境では、setuid bwrapを無理に成立させるより別の方法を採った方がよさそうです。
解決策:Codexを起動するときだけcapabilityを落とす
ここで考え方を変えました。
NET_ADMINが必要なのはコンテナ起動時のfirewall設定です。
Codex自身がNET_ADMINを持っている必要はありません。
ならば、
Podman container
│
├─ NET_ADMINあり
│ └─ iptables設定
│
└─ Codex起動
│
└─ capabilityをすべてdrop
│
▼
bwrap
とすればよいことになります。
Linuxのsetprivを使って試しました。
setpriv \
--bounding-set=-all \
--inh-caps=-all \
--ambient-caps=-all \
bwrap --unshare-user --ro-bind / / true
echo "bwrap-user rc=$?"
結果は、
bwrap-user rc=0
ネットワークnamespaceを含む場合も、
setpriv \
--bounding-set=-all \
--inh-caps=-all \
--ambient-caps=-all \
bwrap --unshare-user --unshare-net --ro-bind / / true
で正常終了しました。
つまり、PodmanやWSL2でbwrapそのものが動かないわけではありません。
Codexに引き継がれていたコンテナのcapabilityがbwrapと衝突していたことが原因でした。
Codex用のラッパーを作る
毎回setpriv付きでCodexを起動するのは面倒なので、Codex用のラッパースクリプトを用意することにしました。
例えば以下のようなスクリプトです。
#!/usr/bin/env bash
set -euo pipefail
exec setpriv \
--bounding-set=-all \
--inh-caps=-all \
--ambient-caps=-all \
"$HOME/.npm-global/bin/codex" "$@"
これを~/.local/bin/codexとして配置します。
PATHは、
ENV NPM_CONFIG_PREFIX=/home/$USERNAME/.npm-global
ENV PATH=/home/$USERNAME/.local/bin:/home/$USERNAME/.npm-global/bin:$PATH
として、~/.local/binをnpm globalより前にします。
これで、
codex
と普通に実行した場合でも、
~/.local/bin/codex
│
└─ setpriv
│
└─ capabilityをdrop
│
▼
~/.npm-global/bin/codex
│
▼
Codex CLI
│
▼
bwrap
という経路になります。
最初はnpmのcodex自体を置き換えていた
実は最初の対策では、npmが生成したcodexをcodex-realへリネームしていました。
mv ~/.npm-global/bin/codex ~/.npm-global/bin/codex-real
そして同じ~/.npm-global/bin/codexにラッパーを置いていました。
この方法でも動作します。
しかし後日、Codex CLIのアップデートが入りました。
コンテナ内でCodexが更新されるとnpmによって~/.npm-global/bin/codexが再生成され、ラッパーが上書きされます。
すると当然ながら、
Codex
↓
NET_ADMINを保持したままbwrap
↓
Unexpected capabilities...
となり、bwrapエラーが再発しました。
ラッパーはnpm管理外へ置く
そこで最終的には、npm管理下のファイルを変更しない構成にしました。
~/.local/bin/codex
↑
│ PATHでこちらを優先
│
└─ 自作ラッパー
~/.npm-global/bin/codex
↑
└─ npmが管理する本物のCodex CLI
これならCodex CLIがアップデートされても、更新されるのは
~/.npm-global/bin/codex
だけです。
~/.local/bin/codexのラッパーはnpmの管理外なので残り続けます。
確認するときは、
type -a codex
で、
codex is /home/node/.local/bin/codex
codex is /home/node/.npm-global/bin/codex
の順になっていれば意図した状態です。
最終的な構成
devcontainer.jsonでは、firewallに必要なNET_ADMINは残します。
"runArgs": [
"--cap-add=NET_ADMIN",
"--userns=keep-id"
]
Bubblewrapにはsetuidやfile capabilityを設定しません。
RUN chown root:root /usr/bin/bwrap \
&& chmod 0755 /usr/bin/bwrap
Codex CLI本体は通常どおりnpmでインストールします。
RUN npm install -g @openai/codex@${AGENT_VERSION}
そしてnpm管理外にラッパーを配置します。
ENV NPM_CONFIG_PREFIX=/home/$USERNAME/.npm-global
ENV PATH=/home/$USERNAME/.local/bin:/home/$USERNAME/.npm-global/bin:$PATH
COPY --chown=$USERNAME:$USERNAME codex-wrapper.sh /home/$USERNAME/.local/bin/codex
USER root
RUN chmod 0755 /home/$USERNAME/.local/bin/codex
USER $USERNAME
codex-wrapper.shは以下です。
#!/usr/bin/env bash
set -euo pipefail
exec setpriv \
--bounding-set=-all \
--inh-caps=-all \
--ambient-caps=-all \
"$HOME/.npm-global/bin/codex" "$@"
まとめ
今回の問題は、最初はCodex CLIのapply_patchの不具合に見えました。
実際には、
- devcontainerのfirewall用に
NET_ADMINを付与していた - そのcapabilityがnodeユーザーにも引き継がれていた
- CodexがBubblewrapを起動した
- non-setuid bwrapがcapabilityを保持したプロセスからの起動を拒否した
という流れでした。
かといってNET_ADMINを削除するとiptablesによるfirewallが使えません。
そこでコンテナにはNET_ADMINを残し、Codexを起動するときだけcapabilityをすべてdropすることで両立させました。
また、npmが管理するcodexを直接ラッパーに置き換えると、Codex CLIのアップデート時に元へ戻ってしまいます。
そのため、
~/.local/bin/codex ← capabilityを落とすラッパー
~/.npm-global/bin/codex ← npm管理のCodex CLI本体
と分離し、PATHでラッパーを優先する形にしています。
Podman、devcontainer、Linux namespace、capability、Bubblewrapと複数レイヤーが絡むため原因の切り分けには少し時間がかかりましたが、最終的にはCodex側へ特別な変更を加えることなく解決できました。
同じように「Codex CLIは動くのにapply_patchだけbwrapで失敗する」という場合は、bwrapそのものを疑う前に、
grep -E 'CapEff|CapBnd|NoNewPrivs|Seccomp' /proc/self/status
でCodexプロセスに不要なLinux capabilityが引き継がれていないか確認してみると、原因を見つけやすいかもしれません。