第21.1章:应用沙盒问题
1. 承诺与陷阱
"一次编写,到处运行"是跨平台开发里最古老的承诺。UWP 对 Windows Phone、Surface RT、Hololens 和 Xbox 许下过这个承诺。鸿蒙的"万物互联"对手机、平板、手表、电视和汽车许下同样的承诺。 它们都只在一个厂商的生态内部才是通用的。UWP 已经死了,WinUI3 半死不活,原因不是技术: 没有开发者能只为一个生态写应用,也没有用户只拥有一个生态。
应用沙盒就是平台把这个生态变成卡点的方式。它被包装成安全机制,确实也提供了隔离,但它同时决定 什么能运行、什么能访问存储、什么能安装、什么能更新。当沙盒围绕厂商的语言和厂商的商店设计时, "跨平台"就变成了"为每个厂商重写一遍应用"。
2. 应用沙盒的五项代价
这里不是说沙盒没用,而是说它不是免费的,账单用可移植性来付。具体代价是:
- 没有共享。沙盒打破了共享库模型。在 Snap 包里,Firefox 启动可能要二十秒左右, 因为运行时和依赖不再像系统包那样共享。每个应用自带更多运行时,二进制膨胀,跨应用访问常用资源 变成一项集成工程。
- 驱动和硬件访问。Flatpak 式的沙盒经常遇到驱动问题。沙盒看不见设备,就用不了设备, 而缺失的功能不是应用在沙盒内部能修好的 bug。
- 安全假象。沙盒隔离的是用户态,不是内核。内核被攻破,沙盒就是一间地板已经没了 的房间周围的栅栏。隔离仍然值得有,但它不是营销暗示的那种绝对保证。
-
开发成本。现有应用要重写,依赖也要重写。有意思的是原因:主流语言根本不是为这个
环境设计的。
fopen和fstream会打开一个文件——打开在哪? 第三方库假设有当前目录、有控制台、有环境变量、有一个从main开始的进程。 在沙盒里,没人知道应用是怎么启动的,也不知道它被允许碰哪些路径。 -
厂商垄断。移动端是 Android/iOS 双头垄断。Windows Phone 和 UWP 是反例:
即使有巨头支持,第三个生态还是失败了。即便在 Android 上,
gcc和clang这类工具也与沙盒结构上不兼容,因为编译器本身就是一个读写任意文件、启动其他进程的进程。
3. C++ 是可移植核心
人们很容易用性能来解释 C++ 的流行。性能是一部分,但更重要的原因是可移植性。一个严肃的跨平台应用, 标准架构是可移植核心 + 平台特定 UI:
- 核心是 C、C++ 或汇编:业务逻辑、编解码器、DRM、密码学、网络、同步、媒体管线、游戏引擎。
- UI 是平台语言:SwiftUI、Jetpack Compose、WinUI、ArkUI,或者一个 WebView。
Spotify、QQ、微信是明显的例子:它们的核心引擎是 C++,外面套着平台特定的壳。核心是花了多年 构建的资产,UI 才是预期会被替换的部分。
Spotify 是一个具体而有用的案例。它在 DRM 和流媒体路径里使用汇编,让播放可以立即开始。 这种"即时串流"是竞争优势,不是可有可无的装饰,而且正是那种不可能为了一个手机平台用托管语言 重写一遍的代码。Windows Phone 7 把第三方应用限制在托管代码里,不允许原生 C++,而 Spotify 的 WP7 应用是在微软的特批下以原生代码运行的,Spotify 也直接向微软投诉。理由很直接: 我们不会仅仅为了支持你们的手机,就重写 Spotify 的核心引擎。这种压力是 Windows Phone 8 改变政策、加入完整 C 和 C++ 支持的部分原因。
同样的道理让 Android 的"不要用 NDK"建议在跨平台产品上是错的。那条建议是写给单平台应用的: 除非有可测量的性能理由,否则用平台语言。但对于一个已经有可移植核心的产品,原生核心才是常态, 平台 UI 才是例外。把核心搬到 Kotlin 或 Swift 不是简化,而是第二份实现、第二套 bug, 而且要永远保持同步。
这就是为什么应用沙盒是一种可移植性税。它攻击的正是那个可移植的层。一个迫使核心改用平台语言、 或者被平台特定的存储和进程 API 锁住的沙盒,把资产重新变成了平台特定的东西。
4. 标准没有的那个类别
第20章给出了标准的两个类别:独立实现可以在没有操作系统的情况下执行; 宿主实现支持文档描述的所有设施。没有第三个类别。尤其是,标准从未定义 受限环境。
Android 是试金石。它有操作系统、调度器、文件系统和网络栈,所以按定义它是宿主实现。 但大多数接触外部世界的宿主设施,行为并不像桌面程序期待的那样。函数存在,权限不存在。
这个区别很重要,因为"宿主"说的是 API 可用性,不是无限制的访问能力。沙盒里的进程可以调用
fopen。调用是否成功是权限决定,不是符合性决定。标准说函数存在,没说程序能打开
任意一个具体路径。
具体来说:
-
fopen("data.txt")和std::fstream{"data.txt"}相对于进程的当前工作目录 解析。在 Android 上,当前工作目录是/,只读。可写的私有目录是Context.getFilesDir(),通常在/data/user/0/<pkg>/files下。 应用特定的外部存储是getExternalFilesDir()。共享媒体走 MediaStore 或存储访问框架 (SAF),返回的是content://URI,不是路径。 -
在 iOS 上,容器是
/var/mobile/Containers/Data/Application/<UUID>/。UUID 在重装、更新或恢复时会变。 硬编码路径从设计上就是坏的。 -
stdout和stderr不是控制台。在 Android 上,printf可能 什么都不做;日志是__android_log_print,fast_io 把它封装成fast_io::android_logger。 -
getenv("HOME")不是应用的数据目录,环境变量也不是契约。tmpfile()和/tmp可能不可写;平台的缓存目录才是正确的位置。 -
main不是入口。Android 代码通过android_main、JNI_OnLoad或ANativeActivity_onCreate进入;exit、atexit和静态析构 可能不会按桌面程序期待的方式运行。没有 shell,也不能随意fork/exec。 -
遗留异常和 RTTI 没有保证。
ndk-build默认-fno-exceptions -fno-rtti;旧的系统运行时两者都没有;AOSP 构建时也不用它们; 跨 JNI 或共享库边界抛异常是不安全的。第19章讲替代方案,而这里正是替代方案重要的又一个地方。 -
std::fstream不能接受文件描述符。它只接受路径。所以即使平台递给你一个句柄—— 来自content://URI 的ParcelFileDescriptor,或者应用私有存储的目录 fd——标准流也用不了。
而且这不是 C++ 独有的问题。没有主流语言的标准 I/O 库是围绕能力(capability)构建的。Java 的
File 和 Path、C# 的 File 和 Directory、
Python 的 open 和 pathlib、Ruby 的 File、Go 的
os.Open、Rust 的 std::fs、Swift 的 FileManager,
全都假设路径、当前目录和环境权限。曾经存在的运行时沙盒被删掉了:Java 的
SecurityManager 已废弃并被永久禁用,.NET 的代码访问安全(CAS)没了,Ruby 的
$SAFE 在 Ruby 3.0 被移除,Python 的 rexec 和 Bastion
早就没了。沙盒搬到了操作系统里,I/O API 却没有跟着搬。
5. 围墙花园与双头垄断
定义。围墙花园操作系统,是禁止直接运行二进制的操作系统。一切皆是 app; 应用从应用商店安装;每个 app 都运行在带权限列表的沙盒里。厂商决定什么能运行、什么能安装、 什么能访问设备。
围墙花园:Android、iOS、iPadOS、ChromeOS、Windows 11 S 模式和 UWP 平台。
不是围墙花园:GNU/Linux、Windows、macOS、FreeBSD、SerenityOS。
这样定义之后,问题就是结构性的,而不是偶发的:
- PWA 是更好的跨平台路径。PWA 就是网站;占用更小,更新不需要用户干预, 运行在浏览器的沙盒里。它不是第二个应用生态,也不是第二套权限模型。
- 安全被用来为削减功能辩护。围墙花园不能运行 Rufus 格式化磁盘,不能给树莓派的 SD 卡刷 Arch Linux,也不能运行一个写任意文件、启动进程的编译器。如果唯一安全的设备是不能运行 你的代码的设备,那这个论证就过头了。"不用手机"不是安全模型。
- 所有权。你买的是设备,不是按厂商条款使用的许可。锁引导加载器(BL)、禁止侧载, 是对所有权的限制,不是安全特性。登录时给一个安全选项才叫选择;锁死 BL 不叫。
- 广告与隐私。浏览器可以跑广告屏蔽器;app 通常不行。app 生态同时也是一个数据市场。 浏览器里的 PWA 不处在同样的位置去卖用户数据,这正是某些厂商更喜欢 app 的原因。
- 沙盒破坏系统级工具。完全应用沙盒限制了操作系统能做什么。磁盘工具、系统安装器、 编译器、终端环境、硬件工具都需要 app 模型不授予的能力。21.3 会讲 Web 侧的对等物,但这里的重点是: "一切皆是 app"不是中立设计。
-
重新适配。现有语言和库不是为这个环境设计的。C/C++ 只有独立和宿主,没有
"受限"类别。一个调用
fopen或fstream打开路径的第三方库打不开文件, 因为路径归沙盒管。iOS 禁止 JIT,所以 Java 一类的运行时在平台层面就被掐死。PWA 也有权限, 但它不要求每个库都围绕权限重写。
UWP 是证明这一点的失败案例。微软想要苹果的抽成模式,试图通过 Windows Phone 和 Windows RT
强推围墙花园。UWP 的权限模型很粗糙,托管优先的设计让 C++ 成了二等公民;Windows Phone 7
则完全不允许原生 C++。大多数应用是 C/C++/汇编写的跨平台逻辑层,
上面套一层薄薄的平台 API(比如 Android 上的 JNI)。UWP 强制托管代码,把文件系统限制到连文件管理器
都没有,很多应用根本没法移植。Windows Phone 用户不能换浏览器,所以没有 Chromium,所以 YouTube
连 PWA 都做不了。微软放弃了 UWP,回到 PWA 和 Win32。那个笑话是准确的:一台 1000 美元的旗舰
Lumia 运行 Windows,却完全不能运行 Windows 的 .exe,因为围墙花园里没有 Win32
子系统。一台在 Walmart 给老人买的 39.99 美元 Android 手机,可以安装 clang,指向微软自己的
Windows 系统根头文件和库,构建一个 PE,再用 Wine 原生运行。昂贵的手机运行 Windows,却不能
运行 Windows 程序;廉价的手机不运行 Windows,却能。微软想要应用商店垄断,胜过想要 Windows
Phone 成为 Windows。Windows 内核不能运行 Windows 的 .exe;Android 能。
Windows Phone 当然会失败。真是个笑话。
双头垄断是同一结构在市场尺度上的表现。Android 和 iOS 控制商店、支付通道、审核流程和 API, 通常还控制浏览器引擎。苹果的 WebKit 强制要求是最明显的例子:iOS 上每个浏览器都必须用 WebKit, 这限制了 Web 在一个自称最注重隐私的平台上能做什么。欧盟的《数字市场法》和日本的规则已经迫使它开放, 但结构仍然是那个结构。
还有一个不那么舒服的点。一个一边抱怨商店抽成、一边拒绝做 PWA 的厂商,本身就是问题的一部分。 文章里的例子是微信:它抱怨苹果税,却不提供 PWA,而且移动网页体验被故意做得比 app 差。 百度贴吧是同一论证的性能版本:Java/Kotlin 的 app 比它自己的移动网页还慢,而移动网页被阉割, 用来逼用户下载 APK。如果一个厂商连 PWA 都不做,它就没有资格抱怨抽成。
沙盒不是中立的安保基础设施。它是平台决定哪些代码被允许存在的边界。一个只在一个厂商的沙盒里运行的 跨平台框架,不是跨平台。它只是一个名字听起来很通用的第一方框架。
6. 维修权是同一场斗争
应用沙盒只是关于所有权的更大争论的软件一半。硬件一半是维修权。Samuel Greengard 在 CACM 的文章 《为维修权而战》 (Fighting for the Right to Repair)(2025 年 9 月 5 日)把联系说得很清楚: 嵌入式软件现在运行手机、家电、汽车、医疗设备和军事装备,而这类软件让制造商和权利持有人更容易 阻止维修、控制售后市场。
"几乎在每一种情况下,嵌入式软件都让阻止维修、控制售后市场变得更容易。这包括锁住设备、 控制谁能修、对替换零件和人工收取更高价格,以及威胁说只要有人打开设备或改动系统就取消保修。"
— Samuel Greengard,《为维修权而战》,CACM,2025 年 9 月 5 日
机制并不隐蔽:
- 锁住设备,控制谁有权维修;
- 对替换零件和人工收取高于市场的价格;
- 威胁说用户打开设备或"改动"系统就取消保修;
- 通过 OTA 更新在售后增加控制,或改变用户协议条款。
这和沙盒是同一个动作,只是低了一层。沙盒说厂商决定哪些代码能运行;维修锁说厂商决定谁能修这台 机器、能装哪些零件。两者都把购买变成了许可。约翰迪尔在美国拖拉机之争里的立场是最纯粹的版本: 农民不拥有拖拉机,他们只有"操作许可"。苹果的零件配对是手机版本:更换屏幕或电池可能被拒绝, 或者被标成"未识别零件",因为它的序列号没有得到厂商的祝福。
维修权运动要求的是显而易见的补救:所有者和独立维修店应该以公平条件获得与授权服务商相同的 零件、工具、文档、诊断和软件访问权。制造商的回答是安全、安保和知识产权。其中一些担忧是真实的; 但没有一个需要维修垄断。一台只能由制造商维修的设备,不是带保修的产品,而是带一块硬件的订阅服务。
法律斗争真实且尚未结束。纽约的《数字公平维修法》、明尼苏达和加利福尼亚的法律、俄勒冈对零件配对的 禁令,以及欧盟的生态设计规则,都是进展,但制造商会游说、起诉,并重新设计来绕开它们。数字锁和 DRM 让规避行为在 DMCA 第 1201 条这类法律下变成非法,即使目的只是维修;最终用户许可协议则声称软件是 许可使用而不是出售。结果是所有者付了钱,却没有控制权。
代价不是抽象的:电子垃圾
环境论证是同一个论证,只是带上了人的身体。下面的 CBS 报道引用的联合国估计是:每年大约产生 5000 万吨电子垃圾,其中大部分没有得到正规回收。很大一部分被运往全球南方,工人们在那里烧电缆、 用酸洗电路板、在垃圾堆里翻找铜、金和稀土。《电子垃圾的悲惨代价》 (CBS Sunday Morning,2023 年 11 月 26 日)跟踪的就是这条产业链。
中国是让这条产业链暴露出来的案例。广东贵屿和浙江台州变成了巨大的非正规拆解镇。工作在 后院和河岸上进行:露天焚烧绝缘层、用酸液提金、手工拆解 CRT。研究者报告了儿童铅中毒和 二噁英污染;2013 年的一项研究估计,当地居民因吸入而面临的癌症风险显著更高,出生结局研究 还发现了更多的死产和低出生体重。整个社区用健康为富裕世界的廉价电子产品付账。
中国最终禁止进口外国电子垃圾:先是 2017 年的"国门利剑"行动,然后到 2021 年全面禁止 固体废物进口。这条产业链没有消失,而是转移了:转到马来西亚、泰国、越南和印度尼西亚, 也转到西非,尤其是加纳阿克拉的阿博布罗西(Agbogbloshie)。同样的焚烧和酸洗跟着垃圾 一起搬了过去。对加纳电子垃圾场附近母亲的研究在母乳中检测到了多氯联苯和阻燃剂,附近产下的 鸡蛋中则检测到了极高水平的二噁英。污染不是局部的麻烦;它进入食物链,也进入下一代。
这就是为什么维修是一项环境政策,而不只是财产权。可维修的设备会继续被使用;被胶封死、 被序列号锁住、被软件抛弃的设备会变成垃圾。应用沙盒也在推这件事:当商店下架一个应用、 系统停止更新,或者厂商拒绝激活替换零件时,一台还能工作的机器就被变成了电子垃圾。 减少电子垃圾最便宜的办法,是一开始就不要把它设计成垃圾。
这就是为什么 PWA 论证不只是关于方便。可维修的设备是你能打开、理解并修好的设备;开放的 Web 应用是 你能检查、托管、修改并安装、不需要守门人的应用。围墙花园在两个层次上是同一个主张:你买下机器之后 厂商仍然拥有它,你安装软件之后厂商仍然拥有软件。如果你不能修这台设备,你就不拥有它。如果你不能运行 你选择的软件,你同样不拥有它。
支持为这件事而战的组织
有三个组织在做这一章所依赖的技术和政治工作。支持它们,并在与监管者沟通时使用它们的材料:
- Open Web Advocacy(OWA) 是一群软件工程师,向监管者和立法者解释守门行为的技术细节。它的优先事项是 #AppleBrowserBan (苹果要求 iOS 上每个浏览器都使用 WebKit)、Web 应用的深度系统集成,以及 Web 应用的平等待遇。 它的报告 《把竞争带回围墙花园》 是解释浏览器引擎竞争为什么重要的最好技术材料。你可以捐款、加入 Discord,或者联系你所在地区的 监管者。
- Keep Android Open 反对 Google 的 Android 开发者验证计划,该计划定于 2027 年开始。根据该计划,每个 Android 应用 开发者——不只是 Play 商店开发者——都必须在 Google 集中注册、付费、接受 Google 条款、提供政府颁发的 身份证明,并提交签名密钥证据。未注册开发者的应用将在全球认证 Android 设备上被阻止,包括侧载安装的 应用和 F-Droid 分发的应用。这场运动的观点很简单:你买的手机不应该变成只能运行 Google 批准软件的 设备。你可以安装 F-Droid、联系监管者、签署请愿书,并传播这场运动。
- Louis Rossmann 是独立维修店老板,也是美国维修权运动最有名的声音。他的 YouTube 频道教授芯片级维修, 并记录那些让维修变得不可能的软件锁、序列化零件和零件配对。他创立了 Repair Preservation Group(501(c)(3),做教育和 Repair Wiki)及其 501(c)(4) 游说基金 Action Fund, 并创立了 FULU Foundation,推动改革 DMCA 第 1201 条, 并为绕过反消费者锁支付维修赏金。你可以支持他的店、wiki,或者 Action Fund。
再进一步:买你能解锁的设备
倡导改变法律,购买改变市场。你投出的最有效一票,是下一次买什么设备。优先选择允许解锁 bootloader、安装另一个操作系统、在厂商停止更新后还能继续使用的硬件。一台 2030 年还能跑 postmarketOS 或 GrapheneOS 的手机,就是一台没有在 2027 年变成电子垃圾的手机。
实用清单:
- bootloader 解锁是官方且文档化的。不需要秘密漏洞,不需要云账号, 不需要"联系客服碰运气",也没有 30 天等待期。
- 固件和恢复镜像公开。更新失败后你能重新刷机;愿意的话,你还能从源码 构建启动链。
- 有 mainline Linux 或受支持的替代系统。postmarketOS、LineageOS、 GrapheneOS,或者有实际设备维护者的发行版——不是一个六个月后就死掉的一次性移植。
- 零件和维修文档可得。电池、屏幕、充电口、板级原理图和维修指南, 而不是一块胶死的三明治。
- 安全更新有公开的结束日期。厂商应该承诺支持窗口,并且更新不依赖运营商。
- 启动固件尽量是自由软件。coreboot、U-Boot、TF-A,或者有文档化解锁路径的 厂商 bootloader。
不错的起点是 Fairphone、Purism(Librem 5)、Pine64(PinePhone),以及运行 GrapheneOS 的 运营商解锁 Pixel。一台刷了 coreboot 的二手 ThinkPad,比一台带固件密码的密封笔记本更值得 长期购买。这些是例子,不是背书;清单比品牌重要。
这就是许可证重要的地方。GPLv2 要求 TiVo 公布它使用的 Linux 内核源码,但没有要求 TiVo 允许 任何人在盒子上运行修改过的内核。bootloader 里的签名检查让源码对修改毫无用处。GPLv3 用 反 Tivo 化 (anti-tivoization)条款补上了这一点:对于"用户产品",分发者必须提供"安装信息"—— 安装并执行修改版本所需的方法、步骤、授权密钥和其他信息。这正是 bootloader 解锁和长寿命设备 背后的法律机制。
这个条款不是通用的解锁法。它适用于消费产品中受 GPLv3 覆盖的代码,不适用于专有 bootloader, 而 Linux 内核仍然是 GPLv2-only。所以实践规则既是法律的也是技术的:优先选择自由软件且提供 安装路径的启动固件,也优先选择 bootloader 真的可以解锁的硬件。一台 bootloader 永久锁死、 却贴着 GPLv3 声明的设备,是新闻稿,不是维修路径。
这些斗争是不同层次上的同一场斗争。OWA 对抗浏览器和应用商店的守门人;Keep Android Open 对抗 操作系统的守门人;Rossmann 和 Repair Preservation Group 对抗维修和零件的守门人。三者都是把 维修权应用到软件和硬件上:所有者决定什么能运行、谁能修、能用多久。PWA 是无需请求许可就能 行使这项权利的实践方式。
7. 转向
如果目标真的是让同一个应用跑在手机、平板、桌面、电视、汽车和头显上,那么已经有一个平台存在于 所有这些设备上,而且不需要厂商许可:Web。渐进式网络应用是带清单和 Service Worker 的网站。 它可以安装,可以离线工作,运行在自己的窗口里,通过 URL 分发,而不是通过应用商店审核。
一个有用的说法是:PWA 就是去掉 W 的 UWP。它保留了通用应用模型,去掉了"宇宙必须属于一个厂商" 这个要求。
常见的反对意见比听起来要弱。"Web 很慢"——WebAssembly 很快,而且 Android 的 Java/Kotlin 也不是原生代码。"Safari 和 Firefox 很差"——那是要求更好的浏览器,不是反对 Web。 "Outlook 和 Teams 的 PWA 很烂"——那些是 WebView 套壳,不是真正的 PWA。"Web 做不了 X"—— Project Fugu 的存在就是为了填这个坑,而且坑在变小。
这一章剩下的部分是建设性的。21.2 搭壳。 21.3 用 WebAssembly 把 C++ 核心放进去。 21.4 在没有守门人的情况下发布。
背景论证在三篇文章里有更长的展开: 为什么 UWP 和厂商的"万物互联" 是伪命题、 什么叫围墙花园,以及 为什么应用沙盒不如渐进式网络应用。 第 6 节的 CACM 文章是同一论证的新闻版本。