Sanitizer 与 CI:让内存错误在合并前暴露
ASan/UBSan/TSan 在机器人 CI 里的启用策略,以及和实时路径、第三方库的取舍。

1. 凌晨三点的 segfault
内存错误在机器人现场表现为「偶发定位跳变」「凌晨三点 segfault」——栈上早已破坏,回溯指向无关函数。Sanitizer 的价值是在合并前把 UB 变成 deterministic crash + 报告,比现场 gdb 省一个数量级时间。
Release 二进制不能带 sanitizer——部署用独立 target,但 merge 要求 nightly asan job 绿。Sanitizer 红 = 合并阻断,不是 warning。asan 覆盖不到的路径等于没测——比不带 sanitizer 更危险,因为团队会误以为「asan 绿了就没问题」。上次底盘上定位偶发跳变,asan 抓到异步 callback 里 dangling reference,lab 里跑了三个月没复现。
2. AddressSanitizer
检测 heap/stack buffer overflow、use-after-free、double free。典型机器人触发点:Eigen::Map 指向已释放的点云缓冲、异步 callback 里捕获 dangling reference。ASan slowdown ~2×,放 nightly 或 PR optional job,别默认进 release 产物。
与 -O1 组合;纯 -O0 有时路径不同,漏报。asan 版链接慢,cache ccache key 要分 sanitizer 维度。ASan 不 catch 未定义行为里的逻辑错——它抓的是内存违规,不是算法错。
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer -g)
add_link_options(-fsanitize=address)
endif()3. ThreadSanitizer 与 UBSan
TSan 抓数据竞争:两线程无同步读写同一地址。SPSC 队列漏 acquire/release、atomic 误用 relaxed 保护复合状态,TSan 能定位两条栈。传感器 pipeline 合并前在 TSan job 跑 10 分钟 bag replay,比 field 上偶发 NaN 便宜。
注意 TSan 与 ASan 通常不能同链——CI 矩阵分两 job。TSan nightly 慢 5–15×,放 nightly 而非 PR 必跑。TSan 看不见 lock-free 算法里「允许」的竞争(若你写错 memory order)。
UBSan 抓整数溢出、空指针解引用、错误 shift。标定参数从 YAML 读 int 再 << 24 前用 uint32_t 显式转换,别赌编译器扩展。控制环里的 fixed-point 换算尤其需要 UBSan——静默 wrap 会导致 wheel odometry 跳变。
4. CI 集成要点
- 失败即红,不 retry 掩盖 flake。
- asan 与 release 共享
ctest -L fast子集,避免 asan 只跑 smoke 而 release 跑全量导致 sanitizer 覆盖不到真实路径。 - nightly
colcon test跑 ASan 全包,PR 只跑-L fast子集但 must 绿。 - MSVC
/fsanitize=address与 GCC 报告格式不同,CI matrix 至少覆盖主 toolchain。
缓存 sanitizer build 目录,但源码变必重编。统一上传 log artifact,方便跨 toolchain 对比报告格式。Release 部署镜像永远不含 sanitizer,但 merge 门禁要求 nightly asan job 绿。
jobs:
asan:
env:
ASAN_OPTIONS: detect_leaks=1:abort_on_error=1
run: |
cmake -B build-asan -DENABLE_ASAN=ON
cmake --build build-asan
ctest --test-dir build-asan --output-on-failure5. 插件与 ODR 边界
dlopen 插件若与 host 各带一份同名全局,可能误报 leak 或 ODR 冲突。CI 策略:host 与 plugin 同一 sanitizer 编译矩阵一起跑。LeakSanitizer 对 dlopen/dlclose 循环有时误报 plugin 内 static singleton「仍 reachable」——给 plugin 提供 plugin_shutdown() 在 dlclose 前释放,LSAN 才干净。
plugin asan 与 host 混链曾 ODR false positive——现在 matrix 强制同 sanitizer 编译。Release 部署镜像不含 sanitizer,但 tag 对应 asan-green commit。插件边界的 sanitizer 策略和主程序一致,否则假阳性会浪费排查时间。
6. 验收
故意注入 bug 验证:UAF 应被 ASan 报出。Release 部署镜像不含 sanitizer,但 tag 对应 asan-green commit。asan OOM 时 ASAN_OPTIONS=allocator_may_return_null=1 便于查 leak。统一上传 log artifact,方便跨 toolchain 对比报告格式。
7. 案例:异步 callback 里的 dangling reference
传感器驱动 callback 捕获 [&] 引用栈上临时变量,executor 延迟调度时变量已析构——ASan 报 stack-use-after-scope,lab 里偶发、field 上必现。改成按值捕获或 shared_ptr<const Frame> 后 asan 绿。这类 bug 不靠 code review 肉眼抓,靠 sanitizer 在合并前阻断。合并前 asan 红就是 merge 阻断,不 retry 不 override。
8. 与 release 路径对齐
asan job 与 release job 用同一 ctest -L fast 子集,避免 asan 只跑 smoke 而 release 跑全量。nightly 全包 asan + PR 子集 asan 是常见分工。Sanitizer 红 = 合并阻断。比现场 gdb 省一个数量级时间——但前提是 CI 覆盖的路径与 release 一致。
9. 选型:哪个 sanitizer 何时跑
PR 必跑 ASan 子集(-L fast),抓最常见的 UAF 和 buffer overflow。nightly 跑 TSan 全包 bag replay,抓数据竞争——这类 bug lab 里偶发、field 高负载必现。UBSan 适合标定和控制环模块,整数溢出和 shift 错误在 release 下静默 wrap。三者不能同链,CI matrix 必须分开 job。Release 部署镜像永远不含 sanitizer,但 merge 门禁要求 nightly 三色全绿。Sanitizer 是合并前的最后防线,不是可选项。比现场 gdb 省一个数量级时间——但前提是 CI 覆盖的路径与 release 一致,不是只跑 smoke。Sanitizer 红即 merge 阻断,不 retry 不 override。nightly 三色全绿是 merge 门禁,Release 镜像永远不含 sanitizer。
相关
也可以看看
johan's blog