OpenAI Codex · 執行邊界

macOS:把安全策略拼成一個字串

Seatbelt 吃的是一段文字。文字由靜態基線加現拼的讀寫網段組成。路徑不進這段文字,走旁邊的參數列。

課程目標讀完能說清三件事。macOS 上平臺沙箱預設就在,沒有 Windows 那種總開關。交給 sandbox-exec 的策略正文只留佔位符,真實路徑走 -D。挖空一條路徑時,要同時擋住這個節點、它的子孫,以及把這個節點搬走。
先玩一遍 · 開關片段,看選單怎麼長
策略拼裝臺:左邊加片段,右邊看正文、配料表和放行範圍
片段
點片段開關,看策略正文和右側探針怎麼變。
網路
實驗
策略正文 · 只留代號
配料表與探針
邏輯軌跡 · 動畫每一步對應原始碼裡的哪一段
  1. 權限檔案拆成讀寫網路manager.rs L375
  2. 固定使用 /usr/bin/sandbox-execseatbelt.rs L56
  3. 裝入四份靜態 sbplseatbelt.rs L21
  4. 按根生成 allow 與 require-notseatbelt.rs L476
  5. 可寫根釘上 file-write-unlinkseatbelt.rs L508
  6. 排除子路徑同時寫 literal 與 subpathseatbelt.rs L551
  7. 拼網路段,managed 無埠走受限seatbelt.rs L309
  8. sections join 成 -p 文字seatbelt.rs L1012
  9. 路徑寫成 -DKEY=valueseatbelt.rs L1024
  10. 雙短橫線後接使用者命令seatbelt.rs L1029
點播放,看一份策略怎樣由片段拼出來。也可自己開關片段。
正文和路徑分開選單上只寫 param 代號。真實目錄名落在 -D。帶引號的路徑汙染不了 SBPL 語法。
挖空少一條就漏只留 subpathmkdir .codex 會成功。兩條 require-not 加上最後的 unlink,才把節點、子孫和搬家一起釘住。
網路先收窄要走代理卻沒有埠,就不要放行整網。網路基線還在,只是沒有 blanket outbound。
教學示意:路徑與探針為課程化設定,用來展示片段如何改變策略文字和放行範圍。邏輯軌跡右側行號對應 openai/codex 倉庫 commit 4f39251a01。
思路一 · 先關大門,再按片段開門
它解決什麼問題

同事把倉庫設成可寫根,讓模型在裡面改程式碼。模型執行 mkdir .codex,想在倉庫裡落一份自己的配置。命令立刻失敗,stderr 寫著 Operation not permitted。可寫根聽起來像整棵樹,實際不是。

再試一條更繞的路。模型先在倉庫裡寫一個普通檔案,再 mv 整個工作區,想把裡面的 .codex 一起挪到沙箱外面。這條也失敗。錯誤還是 Operation not permitted。

思路是什麼

macOS 上 get_platform_sandbox 直接返回 MacosSeatbelt。那個布林開關只對 Windows 有意義。包裝器不是 Codex 自己的 exe,是系統裡固定路徑的 /usr/bin/sandbox-exec。PATH 上放一個同名二進位沒用。

出處:codex-rs/sandboxing/src/manager.rs 第 62 至 76 行 · codex-rs/sandboxing/src/seatbelt.rs 第 52 至 56 行

策略文字分兩層。四份 .sbplinclude_str! 編進二進位,當靜態基線。動態段按這一次的可讀可寫根現拼。拼接順序固定:基線、讀、寫、網路在前。全盤可讀才加 preferences。受限讀才加平臺預設路徑。祖先的 file-write-unlink 放最後,避免前面更寬的 allow 把 rename 用的 unlink 重新開啟。

出處:codex-rs/sandboxing/src/seatbelt.rs 第 21 至 27 行 · codex-rs/sandboxing/src/seatbelt.rs 第 985 至 1012 行

基線第一句業務規則是 (deny default)。沒寫明的事一律拒絕。子行程繼承這份底稿,sandbox-exec 拉起的 bash 再 fork 出來的子孫,仍在同一套規則裡。

權限檔案 讀根 · 寫根 · 網路 四份靜態 sbpl 動態讀寫規則 動態網路段 join -p 文字 -D 參數列 sandbox-exec 再 exec 使用者命令
教學化結構圖:一份權限檔案拆成片段,拼成 argv 再交給系統裡那個固定路徑的包裝器。
為什麼長期成立

Seatbelt 的核心介面就是吃一份 SBPL 文字。能控的是文字怎麼生成。預設拒絕,再按這一次的根往上開門,換語言重寫也用得上。片段的順序是合同:更窄的 deny 必須能壓住前面更寬的 allow。

思路二 · 路徑走配料表,不進選單
它解決什麼問題

有人把工程放在 ~/work/app (copy) 這種帶括號和空格的目錄裡,或者路徑裡出現引號。SBPL 裡括號、引號、分號都是語法。路徑一旦插進策略正文,使用者目錄名就能變成語法。sandbox-exec 解析失敗,模型看到的是包裝器錯誤,不是沙箱拒絕。

思路是什麼

動態讀寫規則只往正文裡寫 (param "WRITABLE_ROOT_0") 這類鍵名。真實路徑寫成 -DKEY=value,成為獨立 argv。排除子路徑同樣走參數列。策略正文和 -D 必須同時出現,測試把這件事鎖成合同。

出處:codex-rs/sandboxing/src/seatbelt.rs 第 1022 至 1028 行

路徑寫進正文 subpath "/tmp/app(copy)" 括號變成語法 包裝器解析失敗 路徑走參數列 正文只寫 param WRITABLE_ROOT_0 -D 單獨一條 argv value 裡可以有引號 語法不被汙染
教學化對照:左邊把目錄名交給解析器,右邊只把代號交給解析器。
為什麼長期成立

安全策略如果最終是一段文字,使用者可控的字串不要進這段文字。模板只含佔位符,真實路徑、域名、埠走另一份參數列。和 Seatbelt 語法無關。Python 裡同樣是模板加 argv。

思路三 · 挖空要擋住節點、子孫和搬家
它解決什麼問題

只寫 subpath,第一次建立這個目錄會漏,mkdir .codex 會成功。中繼資料保護變成空殼。不加 file-write-unlink,rename 能把只讀子樹挪出挖空區。下一次若仍按原路徑授權,實際寫的是連結對面。

思路是什麼

排除子路徑時,require-not 要寫兩條。literal 管目錄自己,subpath 管下面的內容。兩條一起放進 require-all,是並且關係。順序不敏感,少寫 literal 才是。

出處:codex-rs/sandboxing/src/seatbelt.rs 第 548 至 555 行

可寫根預設保護三個頂層名字:.git.agents.codex。可寫根目錄本身再釘一條 deny file-write-unlink。可寫根中間若有使用者可控的符號連結,拼裝直接報錯,不會跟著連結走。

出處:codex-rs/protocol/src/permissions.rs 第 24 至 33 行 · codex-rs/sandboxing/src/seatbelt.rs 第 504 至 509 行 · codex-rs/sandboxing/src/seatbelt.rs 第 441 至 450 行

網路是另一段動態文字。要走代理卻沒有可用埠,返回受限網路段加網路基線,不給 (allow network-outbound) 這種整網放行。網路基線還在,只是沒有 blanket outbound。

出處:codex-rs/sandboxing/src/seatbelt.rs 第 309 至 335 行

literal 擋住這個節點 mkdir .codex 目錄自己還不存在時 subpath 不一定吃到 subpath 擋住它的子孫 建出來之後的檔案 只留 literal 下面的寫會漏 unlink 擋住把邊界搬走 rm 再 ln -s 放在 sections 最後 避免被更寬的 allow 開啟
教學化結構圖:三條規則一起生成,少一條就會漏一種繞法。
路徑走參數列。挖空要三條一起。
為什麼長期成立

挖空一條路徑,同時擋住節點本身和它的子孫。再擋住對這個節點的 rename。三條一起生成,用測試鎖住形狀。跨平臺錯誤文案不同:macOS 常見 Operation not permitted,能被現有關鍵詞表接住。不要假設另外兩個平臺也是這句話。

出處:codex-rs/sandboxing/src/denial.rs 第 50 至 58 行

橫向對比 · 路徑進不進這段文字

Claude Code:先放開讀,路徑 stringify 後進文字

Claude 從 (allow file-read*) 往下加 deny,再在 deny 裡 re-allow。後寫規則優先。路徑用 JSON.stringify 包一層,嵌進策略文字。包裝命令從 PATH 裡找 sandbox-exec,不寫死 /usr/bin。兩邊都要防 rename,Claude 同樣生成 deny file-write-unlink

Codex 從 (deny default) 往上加 allow,路徑留在 -D。付的是生成器複雜度,換來使用者路徑不進入 SBPL 語法。Claude 付的是後寫順序必須按對,換來配置面更接近 allowAllExcept。

兩側均已核對原始碼 · 2026-08-22

DSH 與 Grok:路徑進文字,靠轉義或直接失敗

DSH 預設 (allow default),再 (deny file-write*),只把寫權收回來。路徑經 sbplString 轉義反斜槓和雙引號後,直接嵌進 -p。沒有 -D 參數列。sandbox-exec 從 PATH 取。本機 macOS 上做不到「可寫根裡挖掉 .codex」這一粒度。

Grok 不包一層 sandbox-exec。它把 SBPL 片段交給當前行程。路徑照樣進文字,控制字元直接失敗,避免沙箱報 active、那條路徑卻沒被擋住。核心 apply 失敗時繼續跑。Codex 在 Seatbelt 準備失敗時返回錯誤,命令不會裸跑。

兩側均已核對原始碼 · 2026-08-22
課堂練習
01

少一條 require-not,漏在哪裡

演示裡先開啟寫根和挖空 .codex,看 mkdir .codex 探針是紅的。再開啟「只留 subpath」,探針變綠。用自己的話解釋:subpath 管的是目錄下面,為什麼擋不住第一次建立這個節點。對照 seatbelt.rs 第 548 至 555 行的註釋即可,不要在真實倉庫裡執行破壞性命令。

Takeaway:macOS 上的沙箱是一段拼出來的 SBPL。路徑走 -D,正文只留佔位。挖空要同時擋住節點、子孫和搬家。Seatbelt 擋得住策略裡拒絕的操作,擋不住被寫寬的 allow,也擋不住模型在可寫根裡改業務程式碼。