fix(sandbox): 修复 macOS 白名单沙箱致 /usr/bin xcselect 垫片全灭

8-30 白名单读模型改革后,/usr/bin 下的 python3/git/clang 等 xcselect 垫片
在沙箱内全部报错退出:垫片启动即拉起 xcodebuild 读
/Library/Preferences/com.apple.dt.Xcode.plist 校验 license(被白名单拒绝),
且经 mach 服务 com.apple.bsd.dirhelper 解析 DARWIN_USER_TEMP_DIR
(profile 无 mach-lookup 放行,confstr 失败回退 /tmp)。

- MACOS_MINIMAL_READABLE_PATHS 增加 /Library/Preferences
- 新增 MACOS_BASE_MACH_RULES 放行 dirhelper,只读/可写 profile 共用注入
- 可写 profile 额外放行 $TMPDIR 父目录,容纳垫片 xcrun_db 缓存写入
  (只读 profile 不放行,缓存写失败仅噪音、非致命)

AGENTS.md §10.8 同步补充垫片兼容说明

Co-authored-by: Astrion powered by Kimi-K3 <astrion-agent@users.noreply.github.com>
This commit is contained in:
JOJO 2026-09-07 19:09:58 +08:00
parent d2ff333453
commit d02aeec98a
2 changed files with 27 additions and 0 deletions

View File

@ -427,6 +427,7 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
- Dockerfile 创建 `agent` 用户 + `/etc/gitconfig` safe.directory + 去 setuid 加固;数字 uid 不依赖镜像内用户存在,旧镜像直接受益 - Dockerfile 创建 `agent` 用户 + `/etc/gitconfig` safe.directory + 去 setuid 加固;数字 uid 不依赖镜像内用户存在,旧镜像直接受益
- **macOS 宿主机 = Seatbelt 白名单读模型**`modules/host_sandbox_runner.py` - **macOS 宿主机 = Seatbelt 白名单读模型**`modules/host_sandbox_runner.py`
- 只读/可写两个 profile **共用同一白名单读模型**`_build_macos_whitelist_read_rules`唯一区别是写权限deny default + 系统路径白名单(`MACOS_MINIMAL_READABLE_PATHS`+ 路径授权writable + readable_extra+ 工作区 + 祖先目录 literal allow缺一个祖先进程 exec 直接 Abort trap必须为 file-read*+ env 注入 `GIT_CONFIG_GLOBAL=/dev/null`(可写/只读/持久终端三条 plan 均注入) - 只读/可写两个 profile **共用同一白名单读模型**`_build_macos_whitelist_read_rules`唯一区别是写权限deny default + 系统路径白名单(`MACOS_MINIMAL_READABLE_PATHS`+ 路径授权writable + readable_extra+ 工作区 + 祖先目录 literal allow缺一个祖先进程 exec 直接 Abort trap必须为 file-read*+ env 注入 `GIT_CONFIG_GLOBAL=/dev/null`(可写/只读/持久终端三条 plan 均注入)
- **xcselect 垫片兼容2026-09-07 修复)**/usr/bin 下的 python3/git/clang 等是 xcselect 垫片,启动即拉起 xcodebuild 读 `/Library/Preferences/com.apple.dt.Xcode.plist` 校验 license、并经 mach 服务 `com.apple.bsd.dirhelper` 解析 DARWIN_USER_TEMP_DIR——8-30 白名单化曾致其在沙箱内全灭。现两个 profile 均内置 `MACOS_BASE_MACH_RULES`(放行 dirhelper白名单含 `/Library/Preferences`;可写 profile 额外放行 `$TMPDIR` 父目录xcrun_db 缓存写入),只读 profile 不放行(缓存写失败仅噪音、非致命)
- **deny 规则(.env 正则、~/.ssh 等)必须位于所有 allow 之后**Seatbelt 后规则覆盖先规则);两个 profile 均已修复旧顺序漏洞(工作区 .env 曾实际可读) - **deny 规则(.env 正则、~/.ssh 等)必须位于所有 allow 之后**Seatbelt 后规则覆盖先规则);两个 profile 均已修复旧顺序漏洞(工作区 .env 曾实际可读)
- 可写 profile 已于 2026-08-30 白名单化(此前为全局可读,导致 unrestricted/审批批准后能读授权范围外文件);白名单固有代价:祖先目录顶层文件名可列出(读文件内容仍被拒) - 可写 profile 已于 2026-08-30 白名单化(此前为全局可读,导致 unrestricted/审批批准后能读授权范围外文件);白名单固有代价:祖先目录顶层文件名可列出(读文件内容仍被拒)
- 持久终端 shell plan 支持 readonly 参数(`_build_macos_shell_plan` 复用 `_macos_readonly_profile_for_workspace`):受限档终端以只读 profile 创建unrestricted 保持可写 profile - 持久终端 shell plan 支持 readonly 参数(`_build_macos_shell_plan` 复用 `_macos_readonly_profile_for_workspace`):受限档终端以只读 profile 创建unrestricted 保持可写 profile

View File

@ -38,6 +38,10 @@ class HostSandboxError(RuntimeError):
# /Library/Developer/CommandLineTools 是 Apple git 等开发工具的真身、 # /Library/Developer/CommandLineTools 是 Apple git 等开发工具的真身、
# /opt/homebrew 为 arm64 工具链Intel 的 /usr/local 已由 /usr 覆盖)、 # /opt/homebrew 为 arm64 工具链Intel 的 /usr/local 已由 /usr 覆盖)、
# /private/var 覆盖 $TMPDIR/var/folders/...)。 # /private/var 覆盖 $TMPDIR/var/folders/...)。
# 2026-09-07 增 /Library/Preferences/usr/bin 的 xcselect 垫片python3/git/clang
# 等)每次启动都会拉起 xcodebuild 读取系统许可记录 com.apple.dt.Xcode.plist 验证
# Xcode/CLT license读不到就直接报 "You have not agreed to the Xcode license
# agreements" 退出(该目录在 macOS 默认权限下本就全局可读,不含密钥类敏感物)。
MACOS_MINIMAL_READABLE_PATHS = [ MACOS_MINIMAL_READABLE_PATHS = [
"/bin", "/bin",
"/sbin", "/sbin",
@ -46,6 +50,7 @@ MACOS_MINIMAL_READABLE_PATHS = [
"/System", "/System",
"/Library/Apple", "/Library/Apple",
"/Library/Developer/CommandLineTools", "/Library/Developer/CommandLineTools",
"/Library/Preferences",
"/Applications", "/Applications",
"/etc", "/etc",
"/private/etc", "/private/etc",
@ -57,6 +62,12 @@ MACOS_MINIMAL_READABLE_PATHS = [
"/opt/homebrew", "/opt/homebrew",
] ]
# 两个 macOS profile 共用的基础 mach 规则。dirhelper 是 libSystem 解析
# DARWIN_USER_TEMP_DIRconfstr所必需的服务缺省被拒后垫片会打印
# "confstr() failed with code 5" 警告并把 TMPDIR 回退到 /tmp
# 它只解析/创建当前用户自己的临时目录,不放行任何文件写权限。
MACOS_BASE_MACH_RULES = '(allow mach-lookup (global-name "com.apple.bsd.dirhelper"))\n'
def _expand_path(raw: str) -> Optional[str]: def _expand_path(raw: str) -> Optional[str]:
"""展开路径中的 ~ 并返回绝对路径;无法展开时返回 None。""" """展开路径中的 ~ 并返回绝对路径;无法展开时返回 None。"""
@ -301,6 +312,7 @@ def _macos_readonly_profile_for_workspace(
'(deny default)\n' '(deny default)\n'
'(allow sysctl-read)\n' '(allow sysctl-read)\n'
'(allow process*)\n' '(allow process*)\n'
f'{MACOS_BASE_MACH_RULES}'
f'{network_policy}' f'{network_policy}'
f'{allow_rules}\n' f'{allow_rules}\n'
# deny 必须位于所有 allow 之后(后规则覆盖先规则) # deny 必须位于所有 allow 之后(后规则覆盖先规则)
@ -337,6 +349,19 @@ def _macos_profile_for_workspace(
) -> str: ) -> str:
workspace = str(work_path.resolve()) workspace = str(work_path.resolve())
writable_paths = [workspace, "/tmp", "/private/tmp", "/dev/null"] writable_paths = [workspace, "/tmp", "/private/tmp", "/dev/null"]
# dirhelper 放行后 TMPDIR 解析为真实 per-user 临时目录(/var/folders/.../T/
# xcselect 垫片会向其中写 xcrun_db 缓存,只读/可写 profile 语义不同——
# 可写 profile 放行其父目录(含同级 C/ 缓存目录,与放行 /tmp 的语义对齐,
# DAC 保证仅当前用户自己的目录可写),否则每次垫片调用都刷缓存写失败噪音;
# 只读 profile 不放行,缓存写失败仅噪音、非致命。
tmpdir = os.environ.get("TMPDIR", "")
if tmpdir:
try:
user_tmp_parent = str(Path(tmpdir).resolve().parent)
if user_tmp_parent not in writable_paths:
writable_paths.append(user_tmp_parent)
except Exception:
pass
for raw in get_macos_writable_paths(): for raw in get_macos_writable_paths():
try: try:
expanded = str(Path(raw).expanduser().resolve()) expanded = str(Path(raw).expanduser().resolve())
@ -371,6 +396,7 @@ def _macos_profile_for_workspace(
'(deny default)\n' '(deny default)\n'
'(allow sysctl-read)\n' '(allow sysctl-read)\n'
'(allow process*)\n' '(allow process*)\n'
f'{MACOS_BASE_MACH_RULES}'
f'{network_policy}' f'{network_policy}'
f'{allow_rules}\n' f'{allow_rules}\n'
# deny 必须位于所有 allow 之后Seatbelt 后规则覆盖先规则), # deny 必须位于所有 allow 之后Seatbelt 后规则覆盖先规则),