ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android 上 SQLite 触发 SIGSYS 崩溃排查:从 modernc 换到 ncruces 驱动

Android 上 SQLite 触发 SIGSYS 崩溃排查:从 modernc 换到 ncruces 驱动 场景纯 Go 服务端二进制 Android WebView 壳内嵌 SQLite。问题App 启动后服务端一打开数据库就SIGSYS: bad system call崩溃。根因x86_64 Android 的 seccomp 策略拦截了 modernc.org/sqlite 底层使用的旧式lstat系统调用。解决换用ncruces/go-sqlite3驱动走 Go 标准库 IO不再触发旧系统调用。目录背景与现象排查过程根因分析修复依赖的两条死路正路换 SQLite 驱动持久化与 App 升级验证壳层稳定性修复坑与教训常见问题 FAQ总结1. 背景与现象一个 Android 应用采用“纯 Go 服务端二进制 极简 WebView 壳”的架构。开发机使用 Waydroidx86_64Android 13 API 33作为测试环境。现象text2026/10/04 03:31:41 homecast-go listening on http://0.0.0.0:28976 ... SIGSYS: bad system call PC0x40d00e m0 sigcode1 goroutine 1 [syscall]: syscall.Syscall(0x6, ...) // 0x6 SYS_lstat (amd64) modernc.org/libc.X__syscall2(...) modernc.org/libc._fstatat_kstat(...) modernc.org/libc.X__fstatat(...) modernc.org/sqlite/lib._unixFullPathname(...)关键对比同一个 APKroot 托管服务端时正常root 进程没有 seccomp 过滤App 进程内运行就崩。结论问题出在App 进程的 Android seccomp 安全策略。2. 排查过程2.1 定位责任链textdatabase/sql标准库无责 → modernc.org/sqlite纯 Go 移植 SQLite不直接发 syscall → modernc.org/libcmusl 移植真正发 syscall 的一方 打开库时解析路径 → SYS_lstat老式系统调用x86_64 独有 → Android seccomp只允许 newfstatat 家族modernc的 libc 是ccgo 生成代码musl 原样翻译。_fstatat_kstat为了兼容老 Linux 内核保留了SYS_lstat/SYS_stat老分支。Android 的 seccomp 白名单不认这些老编号 → 直接 SIGSYS 杀进程。2.2 关键发现arm64 无辜查ccgo_linux_arm64.goarm64 版没有 SYS_lstat / SYS_stat 老分支只有SYS_fstat(80)SYS_newfstatat都在 Android 白名单内。所以这是x86_64 Android 独有的坑环境架构modernc 路径结果真机arm64newfstatat / fstat允许干净桌面 Linuxx86_64老 / 新都有Linux 不拦干净Waydroid 容器x86_64老 syscall 被 seccomp 拦崩3. 根因分析Android 使用 seccomp 限制 App 进程可以调用的系统调用。x86_64 平台上lstat和stat是早期编号Android 白名单只认newfstatat家族。modernc.org/sqlite依赖的modernc.org/libc是 musl 的 ccgo 翻译保留了老分支textUnixFullPathname → fstatat → _fstatat_kstat → SYS_lstat老编号Android 看到SYS_lstat不在白名单 → 直接发 SIGSYS。4. 修复依赖的两条死路4.1 Go-overlay构建补丁走不通想通过构建期替换生成文件里的老 syscallbashgo build -overlay patch.json直接报错textFiles beneath GOMODCACHE must not be replacedGo 官方禁止 overlay 替换模块缓存文件此路不通。4.2 上游 issue已关闭未修复gitlab.com/cznic/libc的 issue #412024-08 报的2025-04 被维护者关闭但从未修复git 历史零相关提交最新版 v1.77.1 仍复现因为是 ccgo 生成代码改生成物会被下次生成覆盖。GitHub 上modernc/libc404cznic/sqlite2018 年归档。最后用 fork issue 存档到 GitHub。5. 正路换 SQLite 驱动候选ncruces/go-sqlite3纯 GoWASM 虚拟化 SQLite。文件 IO 走Go 标准库 os 包——Go 自己十几年前就为 Android 修过 stat 问题标准库在 Android 上只用允许的接口。5.1 验证用 strace 跟踪本地跟踪 ≈ 同一二进制在 Android 里的调用面textncruces 版 DB 打开openat(...) newfstatat(AT_FDCWD, ..., AT_SYMLINK_NOFOLLOW) 零个 lstat、零个老 stat —— 全在 Android seccomp 白名单内对照 modernc 版syscall.Syscall(0x6) lstat被毙。5.2 代码改动go/internal/store/db.go换驱动三处改动go// 之前 _ modernc.org/sqlite db, err : sql.Open(sqlite, path) // 之后 _ github.com/ncruces/go-sqlite3/driver db, err : sql.Open(sqlite3, path)5.3 验证结果项目结果37 个单测 -race全绿数据兼容旧库直接读出真实数据SQLite 文件格式一致二进制体积14MB → 3.3MBWaydroid 实测稳定运行零 SIGSYS6. 持久化与 App 升级验证WASM 只是“SQLite 怎么跑”数据照样写真实文件。引擎通过 WASI → wazero → Go 标准库写到磁盘。WASM 沙箱隔离的是“系统调用方式”不是“存储介质”。安卓侧数据落在 App 私有目录textfilesDir/.config/homecast/homecast.dbAndroid 保证私有目录持久化卸载或清数据才丢。三轮实测测试操作结果写入加一条收藏fav/list立刻可见进程重启am force-stop后重启收藏完好App 升级adb install -r覆盖安装收藏完好升级含义以后发新版本直接覆盖安装 APK 即可歌单 / 设置零丢失。7. 壳层稳定性修复调试期踩了两个壳层坑。7.1 ETXTBSY重复打开现象App 每次onCreate都覆盖写 assets 拷出的服务端二进制并重启。后台实例或重复打开时旧进程还映射着该文件直写被内核拒Text file busy。修复启动前先探测端口已就绪则直接复用单实例语义拷贝改「写 tmp →Files.move(REPLACE_EXISTING)」rename 可替换被映射的文件。7.2 主线程网络闪退现象第一版把端口探测放进了onCreate主线程。Android 主线程禁网络StrictMode抛NetworkOnMainThreadException→ 一点开就闪退。修复启动全流程探测 → 启动 → 轮询 →load移入后台线程主线程零网络loadUrl回主线程执行。8. 坑与教训go build -overlay不能替换 GOMODCACHE 下的文件官方硬限制报错前会静默忽略导致补丁根本没进二进制。pkill -f xxx会匹配命令行自身bash 命令行里含那个字符串就把自己杀了命令输出全空。Waydroid 图形会话要 Wayland / Xheadless shell 里waydroid session start起不来。刷 issue 前先搜上游已有 issue已关闭未修复。strace 过滤要谨慎strace -e trace%stat在某些版本抓不到调用直接全跟踪再 grep 更稳。adb exec-out screencap -p的 stderr 会混进 PNG重定向没写干净会把二进制文件搞坏。Android 主线程禁网络任何 HTTP 探测必须放后台线程否则一点开就闪退。覆盖写正在执行的文件 ETXTBSY可执行文件被进程映射时直写被内核拒标准做法是写 tmp 再 rename。9. 常见问题 FAQQ1为什么 modernc 版本会触发 SIGSYSmodernc 的 libc 是 musl 的 ccgo 翻译保留了对老式lstat/stat的调用。x86_64 Android 的 seccomp 白名单不认这些老编号直接杀进程。Q2为什么 arm64 真机没事arm64 版没有SYS_lstat/SYS_stat老分支只有fstat和newfstatat都在 Android 白名单内。Q3ncruces 版本为什么不会崩它的文件 IO 走 Go 标准库os包。Go 标准库在 Android 上只用允许的接口不触发旧系统调用。Q4数据会丢吗不会。SQLite 文件格式一致旧库可以直接读。数据写在 App 私有目录覆盖安装保留。Q5换驱动后二进制变小了从 14MB 降到 3.3MB因为 ncruces 版本比 modernc 更精简。Q6Android 主线程为什么不能做网络AndroidStrictMode会抛NetworkOnMainThreadException这是RuntimeException普通 try-catch 抓不住。Q7如何验证 App 内嵌服务端是否真的在跑用adb forward把端口映射出来直接请求验证bashadb forward tcp:28976 tcp:28976 curl http://127.0.0.1:28976/Q8升级 APK 会不会清数据不会。adb install -r覆盖安装保留私有数据。只有卸载或手动清数据才会丢。10. 总结核心结论x86_64 Android 的 seccomp 不认旧式lstatmodernc 会崩arm64 不受影响因为它只用fstat/newfstatat换ncruces/go-sqlite3走 Go 标准库 IO彻底规避数据持久化没问题覆盖安装保留卸载才丢壳层注意 ETXTBSY 和主线程网络。一句话x86_64 Android 上跑纯 Go SQLite避开 modernc选 ncruces数据写私有目录覆盖安装不丢。本文基于 Waydroid Go SQLite 实战排查整理。不同 Android 版本、Go 版本和驱动版本下细节可能不同请结合实际调整。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进