在 2026 年 9 月的安卓(Android) 17 版本中,当拒绝字符集里有两个及以上均小于 U+0100 的字符时,wcscspn() 和 wcspbrk() 会在字符串中首个大于等于 U+0100 的字符处停下,并把它当作匹配字符,即便该字符并不在集合里;wcsspn()、wcschr()、wcsrchr()、wmemchr() 和 wcsstr() 仍正常。任何用 ASCII 分隔符(例如换行符或通配符)扫描宽字符串的代码,只要文本出现表情符号、中日韩文字、西里尔字母、希腊字母或拉丁扩展字符就会中断。
正文
描述
在 2026 年 9 月的 Android 17 构建中,当拒绝字符集合包含两个及以上、且全部小于 U+0100 的字符时,wcscspn() 和 wcspbrk() 会在字符串中遇到第一个大于等于 U+0100 的字符时停下,并把它当作匹配项返回,即便该字符并不在集合里。wcsspn()、wcschr()、wcsrchr()、wmemchr() 和 wcsstr() 则表现正常。
因此,任何用宽字符串扫描 ASCII 分隔符(例如换行符或通配符)的代码,只要文本里出现 emoji、中日韩文字、西里尔字母、希腊字母或拉丁扩展字符,就会出错。
受影响/不受影响
| 设备 | 构建 | 结果 | | --- | --- | --- | | Pixel 7 | google/panther/panther:17/CP3A.260905.009/16091614:user/release-keys | broken | | Pixel 8a | google/akita/akita:17/CP3A.260905.009/16091614:user/release-keys(2026-09-05 补丁) | broken | | Nothing Phone 2 | Nothing/PongEEA/Pong:16/BQ2A.250721.001-BP2A.250605.031.A3(2026-08-01 补丁) | ok |
公开的 android17-release 源码(2026 年 6 月的 android-17.0.0_r1)中,wcscspn/wcspbrk 来自 external/llvm-libc,实现是对 wchar_t 的朴素嵌套循环,是正确的。9 月构建里的代码则不同(见“机制”一节),说明回归是在 6 月到 9 月发布之间引入的。
复现代码
用 NDK(aarch64-linux-android30-clang)编译后推送到 /data/local/tmp,再用 adb shell 运行。测试程序把宽字符串 s 初始化为 {'a', 0x1F680, 0x100, 'b', 'x', '\n', 0},并打印 wcscspn(s, L"x\n")、wcspbrk(s, L"x\n") 和 wcsspn(s, L"a\U0001F680") 的结果。
在 CP3A.260905.009 上,输出是:wcscspn(s, L"x\n") 为 1(预期 4),wcspbrk(s, L"x\n") 的索引为 1(预期 4),而 wcsspn(s, L"a\U0001F680") 为 2(预期 2)。
对 U+0001 到 U+10FFFF 的每一个码位(跳过代理区和两个集合成员)用 wcscspn(L"<cp>\n", L"x\n") 扫描时,有 1,111,808 个码位返回 0 而不是 1:也就是从 U+0100 到 U+10FFFF 的全部码位。U+0001 到 U+00FF 则正确。
在 Pixel 8a 上测得的触发条件如下:
| 拒绝字符集合 | 结果 |
| --- | --- |
| 单个字符 | ok |
| 两个及以上字符,且全部 < U+0100(例如 L"x\n"、L"\xe9\n") | broken |
| 两个及以上字符,且至少有一个 >= U+0100 | ok |
| 被扫描字符串限制在 U+0080..U+00FF | ok |
机制(来自已发布的 libc.so)
在 CP3A.260905.009 上,反汇编 /apex/com.android.runtime/lib64/bionic/libc.so 中的 wcscspn 可以看到,函数会根据集合走三条路径:
- 单个字符:普通的比较循环,正确。
- 两个及以上字符:在栈上建一张 256 字节的
bool表,用集合中处于 1..0xFF 的字符填充;如果遇到 >= 0x100 的集合字符,函数就退回对被扫描字符串中的每个字符调用wcschr()。这部分正确。 - 其余情况则对照该表扫描被扫描字符串:先用
ldrb w8, [x9, x8]取table[c],用tbnz w8, #0, a2578判断若命中就停止(正确);接着用ldr w8, [x22, #0x4]!取下一个字符,若c < 0x100就通过b.lo a2564继续扫描;可一旦c >= 0x100,代码也会跳到停止处,并把它报告为匹配。
由于成员全部 < 0x100 的集合中不可能包含 >= 0x100 的字符,对 wcscspn 来说扫描应当越过它继续,但代码却停了下来。wcsspn 的结构相同,而在那里停下来是正确的(该字符不在保留集合里),这正是只有补集函数出错的原因。wcspbrk 基于这一扫描实现,因此继承了 bug。
现有的 bionic 测试 TEST(wchar, wcspbrk)(位于 tests/wchar_test.cpp)只用了 ASCII 输入,所以没能发现问题。如果改成类似 EXPECT_EQ(3u, wcscspn(L"aĀb\n", L"x\n")) 的测试就能捕获。
实际影响
Android 版 Visual Pinball(libwinevbs,Wine 的 vbscript 引擎的 Unix 构建)在 9 月更新后,无法加载任何 VBScript 源码注释里带 emoji 的表格:词法分析器用 wcspbrk(ptr, L"\n\r") 跳过注释,结果拿到的是第一个 emoji 的位置而不是行尾,于是报出 “Invalid character”。FileSystem.MoveFolder 同样通过 wcspbrk(src, L"*?><") 把每个含 >= U+0100 字符的路径当成通配符模式。
下游已用普通循环替换这些调用作为规避:vpinball/libwinevbs#41(随 vpinball/vpinball@e7837322f4 发布)。
受影响版本
r29
评论
讨论气氛充斥着对 Google 收紧 Android(AOSP)源码发布的失望与嘲讽,多数人抨击其控制欲并呼吁直接分叉或转向尊重自由的替代系统。但分歧同样明显:一部分人将延迟归因于欧盟法规的意外反噬,另一部分人或认为 AI 逆向已让“不发布”失去意义,或强调即便拿到源码也收益有限、AOSP 的开放模式正走向过时。
认为应当立刻分叉 Android 或转向尊重自由的替代系统
- 面对 Google 的控制欲和对源码发布的收紧,社区普遍认为继续依赖官方 Android 已无意义,应当直接分叉项目或打造替代方案;有评论形容每一代控制狂最终都会对 Android 感到厌倦,而市场真正需要的功能早已被 iOS 合并,不如彻底放弃。(较多人)
- > 我们需要一个尊重用户自由的 Android 替代品,Google 的新座右铭似乎是“尽量作恶”。(@reddalo)
认为 Google 的动机与工程质量已沦为笑话
- 不少人对 Google 表示强烈蔑视,认为其企业激励机制已扭曲到成为笑柄,并吐槽公司频繁出现明显缺乏思考的低级 Bug;这种调侃也代表了社区对 Google 当下状态的普遍情绪。(较多人)
- > 企业激励机制已经彻底沦为笑柄。(@jackdaniel)
将 AOSP 更新延迟归因于欧盟法规的意外反噬
- 少数评论提出了一种反直觉的推测:Google 故意推迟公开 AOSP 更新,并非只是单纯的封闭策略,而是因为欧盟法规规定“源码发布”即被视为更新发布并开始计算支持倒计时;为了让三星等厂商有更多时间基于 AOSP 完成适配,Google 只能延迟公开源码。(少数人)
- > 欧盟法规把源码发布当作更新起点并开始计时,Google 推迟公开 AOSP 其实是在给三星等厂商争取适配时间。(@kodebach)
认为 AI 与逆向工具让“不发布源码”失去意义
- 在 AI 时代,Google 试图靠不发布源码来维持控制的做法被认为已经失效;评论指出 AI 正在被用于“打开”(逆向或重建)软件源码,甚至有人询问专用逆向工程工具的实战体验。(较多人)
- > 在 AI 时代,AI 正在成为“打开”软件源码的利器,这是目前为止最棒的用途之一。(@userbinator)
认为即便拿到源码也收益有限,AOSP 开放模式正逐渐过时
- 有反驳观点指出,单纯拿到 AOSP 源码其实帮助不大:NDK 等核心组件长期人力不足(像个只有两人的兼职项目),历史问题不会因开源而消失;同时随着软件供应链经济变化,减少软件物料清单(SBOM)和攻击面、走向垂直整合,比复用数百个依赖更有价值,AOSP 模式的弊端已超过收益。(少数人)
- > NDK 长期只有约两名全职人力维护,即便拿到源码也解决不了它的固有问题。(@pjmlp)
有价值的补充
- 讨论中提到了一款名为 “REA Reverse – Engineer Anything” 的逆向工程工具,并有评论好奇其实际使用体验,显示出社区对自动化逆向方案的关注。
- 有评论通过一段极简的“纯比较循环”代码,察觉出 AI(带有明显的 Claude 风格)参与重写源码的痕迹,并确认这段代码虽然只有一个字符的差异但完全正确,是 AI 用于逆向的一个具体例证。