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:
parent
c80bc4fbb4
commit
b4deee0f43
@ -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
|
||||||
|
|||||||
@ -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_DIR(confstr)所必需的服务,缺省被拒后垫片会打印
|
||||||
|
# "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 后规则覆盖先规则),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user