行业#逆向工程

AOSP 源码发布滞后到开发者反编译 libc 才能报 bug:Android NDK 宽字符函数现缺陷

Android 17 的 bionic libc 误判不小于 U+0100 的宽字符;AOSP 源码滞后月余,报告者反编译 libc.so 才定位,修复未落地。

倾斜虚化的屏幕特写,彩色源代码行铺满深色背景

Android 17 的系统 C 库 bionic 藏着一个判断颠倒的宽字符缺陷。10 月 12 日,开发者 francisdb 在 Android NDK 的 issue 跟踪器开出 #2260:wcscspn 与 wcspbrk 在拒绝集含两个及以上、全部小于 U+0100 的字符时,会把字符串里第一个不小于 U+0100 的字符误判成命中——汉字、emoji、西里尔字母全部中招。他在 Pixel 7 与 Pixel 8a 的 9 月版系统镜像(build CP3A.260905.009)上复现,又拿 Nothing Phone 2 的 Android 16 对照确认正常。由于 AOSP 公开分支还没有这份实机镜像对应的源码,他最后靠反编译设备上的 libc.so 才把问题钉死。

要点

  • 触发条件已经量化:拒绝集有两个及以上、全部小于 U+0100 的字符时,U+0100 到 U+10FFFF 共 1,111,808 个码点全部被误判为匹配;wcsspn、wcschr、wcsstr 等同族函数不受影响(报告者做了全码点扫描)。
  • 波及面有多份实证:Pixel 7/8a 的 9 月镜像(补丁级别 2026-09-05)复现,Nothing Phone 2 的 Android 16 正常,NDK r29 被标记为受影响版本;Visual Pinball 的表格文件因 VBScript 注释带 emoji 而无法加载。
  • Google 协作者、bionic 维护者 enh-google 当日确认树顶(ToT)仍有此问题,还发现既有 wcscspn 测试的期望值本身写错;截至 10 月 14 日 issue 仍开放,Gerrit 上没有已合并的修复。

背景

源码发布节奏是这起报告的暗线。enh-google 在回复里承认,他想说「你可以直接看源码」,却发现 android-latest-release 这个 manifest 分支旧得装不下 4 月的改动;pirama-arumuga-nainar 补充说 android17-release 这个 git 分支里有 bionic 源码;zenczykowski 紧接着反问:真正跑在 Pixel 上、已经稳定发布一个多月的 Android 17 QPR1,源码在哪里?报告本身也给出了证据:公开的 android17-release(android-17.0.0_r1,2026 年 6 月)里,这两个函数来自 external/llvm-libc 的朴素双重循环,没有这个 bug——实机 9 月镜像里的代码已经换成了另一套。

开发者对 Google 开源生态的耐心本就在磨合期。本站 10 月初写过 Google 冻结开源漏洞赏金:AI 生成的无效报告把审核通道挤爆,产品漏洞提交自 10 月 1 日起暂停。两周后,又一个基础库问题只能靠志愿者自己动手才能进入 Google 的视野。

缺陷机理

wcscspn(s, set) 返回 s 开头不含 set 中任何字符的最长长度,wcspbrk 返回首个命中位置的指针,两者是 C 标准里扫宽字符串的常用组合。报告者把触发条件收敛成了一张表:

拒绝集形状wcscspn/wcspbrk
单个字符正常
两个及以上,全部 < U+0100(如 L"x\n")触发
两个及以上,含 ≥ U+0100 的字符正常

全码点扫描的结果更直白:对每个码点跑 wcscspn(L"<cp>\n", L"x\n"),U+0001 到 U+00FF 都正确,从 U+0100 到 U+10FFFF 的 1,111,808 个码点全部返回 0 而非 1。最小复现只要一行数据:wchar_t s[] = {'a', 0x1F680, 0x100, 'b', 'x', '\n', 0},在受影响镜像上 wcscspn(s, L"x\n") 返回 1,正确值是 4。

受影响范围按设备核过:

设备镜像结果
Pixel 7CP3A.260905.009受影响
Pixel 8aCP3A.260905.009(补丁 2026-09-05)受影响
Nothing Phone 2Android 16,BQ2A.250721.001-BP2A.250605.031.A3正常

反编译定位

公开源码对不上实机,定位只能走反汇编。/apex/com.android.runtime/lib64/bionic/libc.so 里的 wcscspn 按集合形状走三条路径:单字符走朴素比较循环;集合里出现 ≥ 0x100 的字符时退回逐字符调 wcschr;其余情况在栈上建一张 256 字节的布尔表,扫描循环每步先查表判断命中,再用 cmp w8, #0x100 配 b.lo 决定是否继续。问题就在这个分支上:「字符 ≥ 0x100」被直接送进了「命中、停止」的出口——而一张全由小于 0x100 的字符组成的表里根本不可能有它。wcsspn 的结构相同,但在保留集语义下「停止」恰好是对的,所以同一段代码只毒死了互补方向的 wcscspn;wcspbrk 在 enh-google 贴出的实现里是 wcscspn 的一层一行转发,随之失守。

实际影响

最先撞上的是 Visual Pinball。它的 libwinevbs 是 Wine 的 VBScript 引擎在 Unix/Android 上的移植,9 月更新后无法加载任何 VBScript 注释里带 emoji 的表格:词法分析器用 wcspbrk(ptr, L"\n\r") 找行尾,拿回来的却是 emoji 的位置,于是报「Invalid character」。另一个症状藏在文件操作里——FileSystem.MoveFolder 靠 wcspbrk(src, L"*?><") 识别通配符,路径里只要含一个不小于 U+0100 的字符,整条路径就被当成通配符模式。

下游的修复比 issue 还早一天。10 月 11 日,另一位开发者 jsm174 用普通循环替换这两处调用,PR 在 libwinevbs 以 commit 7c5c2ce 合并;Visual Pinball 主仓库随后以 commit e7837322f4 升级依赖跟进。两个作者、两个仓库先后撞见同一问题,从下游侧印证了 bug 真实存在。

各方说法

Google 侧的回应很快。enh-google 在 issue 开出几小时后确认「树顶上仍是问题」,并承认既有的 wcscspn 测试期望值本身是错的——测试断言 wcscspn(L"he\ufffelo", L"lo") 等于 2,正确答案是 3,等于把 bug 行为固化成了预期。他的计划是补上缺失的非 ASCII 测试、修正错误测试,并让测试至少跑在另一个 libc 上,同时尝试把修复回移以减少受影响设备。

线程里的争论逐渐偏离函数本身。joaquinvacas 认为自 Google 收紧 AOSP 访问以来「问题在滋生」,而 Google 缺少透明度;pirama-arumuga-nainar 与 zenczykowski 就「哪个分支才算源码到位」各执一词。Hacker News 上的讨论(10 月 14 日提交,截至 10 月 15 日 480 分、234 条评论)分了几路:kodebach 和 izacus 猜测源码延迟与欧盟《网络弹性法案》(Regulation (EU) 2024/2847)的更新时限义务有关,hobofan 用「三星基于 Android 17 的 One UI 9 已先发一个月」反驳,bmicraft 质疑「单台设备已更新、源码却扣着」在逻辑上说不通;自称前 Google Android 员工的 refulgentis 把原因归给「MBA 式管理越来越多,在乎的人越来越少,资源越抽越薄」。userbinator 和 StilesCrisis 则怀疑报告文本出自 Claude 之手。以上均为论坛观点;issue 里的复现代码、设备矩阵和反汇编是可核对的技术证据。截至发稿,没有检索到传统媒体的独立报道。

修复进展

修复还没有落地。截至 10 月 14 日,issue 仍开放,Gerrit 上搜不到已合并的 wcscspn 修复,enh-google 的补测试与回移计划也没有给出时间表。下游等不了官方节奏——libwinevbs 那个「换成普通循环」的改法,任何项目都能照抄,改动量以行计。

编辑判断

对做 Android 原生开发的团队,检查清单很短:grep 一遍代码里的 wcscspn 和 wcspbrk,凡是拿它们扫 ASCII 分隔符(换行、通配符、路径分隔符)的地方,都可能被一个 emoji 或汉字击穿;临时替换成普通循环成本极低,francisdb 给的测试断言 EXPECT_EQ(3u, wcscspn(L"aĀb\n", L"x\n")) 值得直接搬进 CI。

更大的教训在流程侧。同一个 Google,一边为 Chrome 全链利用开出 25 万美元赏金(本站写过 XBOW 用 GPT-5.6 六天拼出 Chrome 全链利用),一边让一个基础 libc 函数的回归只能靠志愿者反汇编实机固件来报告。当公开源码与实机镜像脱钩,报 bug 的门槛就从「读源码」抬到「读机器码」,维护者那句「你可以直接看源码」说到一半卡住,正是这个变化的注脚。接下来值得盯两个日期:修复何时进入月度镜像,QPR1 源码何时公开。