macOS 沙盒签名历险记(或者说 v1.4.0 到 v1.4.5 为什么会存在)
如果你今天翻我们的更新日志,看到一天之内发了六个补丁版本,大概会好奇出了什么事。简短的答案是:macOS 代码签名和 App Sandbox 差点把我们搞垮。下面是完整的经过。
MailVault 在引擎盖下是怎么工作的
MailVault 是一个 Tauri v2 桌面应用。前端是跑在原生 webview 里的 React,后端是一个用 Bun 编译成原生二进制的 Node.js Express 服务。这个二进制(称作“sidecar”)与主应用一起运行,负责所有 IMAP/SMTP 通信。
在 macOS 上分发,Apple 要求应用用 Developer ID 证书签名并做公证。我们还启用了 App Sandbox — Apple 的安全层,限制应用能访问你系统上的哪些东西。它是 Mac App Store 的硬性要求,对任何桌面应用也都是好做法。
问题
发布 v1.4.0 之后,我们发现 sidecar 服务在正式构建里起不来。应用能正常启动,但后端会以退出码 133(SIGTRAP)静默崩溃。开发环境里一切完美 — 只有已签名、已沙盒的正式构建受影响。
我们花了好几轮才一层层剥开:
- v1.4.0 — 服务绑定在
0.0.0.0,前端却连localhost,造成 IPv4/IPv6 不匹配。改成显式绑定127.0.0.1后解决。 - v1.4.1 — “添加账户”对话框直接用了裸的
fetch('/api/test-connection'),而不是 API 辅助函数。在 Tauri 的发布构建里,webview 的源是tauri://localhost,不是http://localhost— 相对 URL 于是解析到了错误的地方。 - v1.4.2 — 缺少
com.apple.security.network.server权限。沙箱阻止了 sidecar 监听端口。 - v1.4.3 — 我们有一段基于 shell 的进程清理(
lsof、kill),在 App Sandbox 里是被禁的。改用了 Tauri 的CommandChild句柄。 - v1.4.4 — 真正的元凶:Bun 编译出的二进制需要 JIT(即时编译)。JIT 编译器会在运行时分配可执行内存。macOS 虽然有权限可以允许这件事(
com.apple.security.cs.allow-jit),但 Tauri 用--deep给应用包里所有二进制签的是同一套权限。结果 sidecar 也带上了 App Sandbox 权限,即便 allow-jit 标志在,JIT 照样被挡住。 - v1.4.5 — 修好了签名流程。sidecar 现在单独签名,用它自己的权限(不带 App Sandbox,允许 JIT)。主应用保留 App Sandbox。签名顺序很关键:先签 sidecar,再签主应用包,而且不能带
--deep,这样才能保住 sidecar 的权限。同时还发现我们的 CI 流程上传的是 Tauri 自动生成的未签名 DMG,而不是我们签好的那个。
修法
解决方案分两部分:
- 各自的权限文件 — sidecar 用
entitlements-sidecar.plist(JIT + 网络,无沙箱)。主应用用entitlements.plist(沙箱 + JIT + 网络)。 - 正确的签名顺序 — 先用 sidecar 自己的权限签 sidecar,再签主
.app包,且不带--deep。用--deep会递归重签包里的一切,把 sidecar 的权限覆盖掉。
如何自己验证签名
如果你想在自己的机器上确认 MailVault 已正确签名并完成公证,打开终端运行这些命令:
1. 验证应用签名:
codesign --verify --verbose /Applications/MailVault.app
你应该看到:
/Applications/MailVault.app: valid on disk /Applications/MailVault.app: satisfies its Designated Requirement
2. 查看主应用的 entitlements:
codesign -d --entitlements - /Applications/MailVault.app
看 com.apple.security.app-sandbox 是否为 true — 这就确认了应用运行在沙箱里。
3. 查看 sidecar 的 entitlements:
codesign -d --entitlements - /Applications/MailVault.app/Contents/MacOS/mailvault-server
sidecar 应该有 com.apple.security.cs.allow-jit,但不应该有 com.apple.security.app-sandbox。
4. 验证公证状态:
spctl --assess --type execute -v /Applications/MailVault.app
你应该看到:
/Applications/MailVault.app: accepted source=Notarized Developer ID
5. 查看 DMG 的公证状态(安装之前):
spctl --assess --type open --context context:primary-signature -v ~/Downloads/MailVault-v1.4.5.dmg
得到的教训
- 尽早测试发布构建。开发构建完全跳过代码签名和沙箱。“开发环境能跑”和“发布版能跑”之间的落差可能极大。
- sidecar 需要特殊对待。如果你在 Tauri 应用里打包了 Bun/Node/Deno 编译出的二进制,它需要自己的权限文件。Tauri 原生不支持逐个二进制配置权限 — 你必须在构建之后重新签名。
--deep很危险。它用起来省事,但会把所有嵌套签名统统覆盖成同一套权限。如果不同的二进制需要不同的权限,就得分别签。- 本地和 CI 用同一个构建脚本。本地构建和 CI 各有一套签名逻辑,结果 CI 上传了错误的(未签名的)DMG。现在两边用的是同一个脚本。
如果你受到那些坏掉的版本影响,我们为此致歉。v1.4.5 是稳定、正确签名的版本。你的邮件从未处于风险之中 — 问题纯粹在于应用二进制没能启动它的后端服务。