首页
/ OpenSSL 构建与测试中的 Sanitizer 指南:ASan、UBSan 与 MSan 的完整实战

OpenSSL 构建与测试中的 Sanitizer 指南:ASan、UBSan 与 MSan 的完整实战

2026-09-09 22:37:31作者:明树来

Sanitizer(编译器插桩工具)可以在程序编译阶段注入运行时检查,用于在测试执行期间自动捕获内存错误、未定义行为与未初始化内存访问等问题。OpenSSL 官方仓库通过 NOTES-SANITIZERS.md 提供了针对 AddressSanitizer(ASan)、UndefinedBehaviorSanitizer(UBSan)和 MemorySanitizer(MSan)三种 sanitizer 的完整使用说明。本文以该文档为骨架,结合仓库中 Configure 脚本、test/README.mdNOTES-VALGRIND.md 的源码级细节进行扩充,帮助读者掌握在 OpenSSL 开发、测试与 CI 环境中正确启用、组合、调优 sanitizer 的完整方法,并能够准确解读 sanitizer 报告,将其作为 Valgrind 之外的高效补充手段。

为什么 OpenSSL 需要 Sanitizer

OpenSSL 是一套以 C 语言实现的通用 TLS 与密码学库,涉及大量指针运算、内存分配/释放、位操作与平台相关汇编代码,恰好是 use-after-free、堆缓冲区溢出、整数溢出等内存类缺陷的高发区域。传统上这类问题依赖 Valgrind 在虚拟机中模拟执行来发现,但 Valgrind 速度慢,且无法覆盖所有错误类型(例如未定义行为、部分整数溢出场景)。

Sanitizer 的核心价值在于:

  • 编译期插桩:在构建时由编译器(GCC 或 Clang)自动注入检查代码,运行时即时报错;
  • 速度优势:通常只有约 2 倍左右的性能开销,远低于 Valgrind 的 10~50 倍减速(数据来源:NOTES-SANITIZERS.md 中的对比表);
  • 能力互补:ASan 负责内存错误与泄漏,UBSan 负责未定义行为,MSan 负责未初始化内存,而 Valgrind 的 memcheck 则在不需要重编译的场景下作为兜底。

OpenSSL 将 sanitizer 集成到其配置系统(./config)中,作为一等公民的构建选项提供,而非让开发者手工拼接编译参数,这大大降低了接入门槛。

三种 Sanitizer 的能力与适用场景

Sanitizer 检测目标 典型错误示例
ASan(AddressSanitizer) 内存错误与泄漏 use-after-free、堆/栈缓冲区溢出、内存泄漏
UBSan(UndefinedBehaviorSanitizer) 未定义行为 整数溢出、空指针解引用、类型不匹配
MSan(MemorySanitizer) 未初始化内存 读取未经初始化(含未初始化 malloc 堆内存)的值

三者关注的问题域互补:ASan 关注"内存的越界与生命周期",UBSan 关注"语言层面未定义行为",MSan 关注"值的未初始化传播"。

版本与平台要求

需求项 说明
编译器 GCC 4.8+ 或 Clang 3.1+ 可启用 ASan 与 UBSan;MSan 仅 Clang 支持(GCC 未实现 MemorySanitizer)
操作系统 Linux、macOS 等受支持平台;MSan 仅支持 Linux
特殊限制 macOS 上暂不支持泄漏检测(LSan),LSan 默认作为 ASan 的一部分运行,因此在 macOS 上需要留意泄漏报告缺失

从源码角度看,这些约束与配置脚本的行为是吻合的:Configureasanubsanmsan 三者默认均处于禁用状态(见 Configure 中的 %disabled 表),只有当用户显式开启时才注入对应编译选项。

启用 Sanitizer 构建 OpenSSL

OpenSSL 的 sanitizer 支持完全内建在配置系统中。./config 脚本最终会调用 Configure,而 Configure 会在检测到 enable-asan/enable-ubsan/enable-msan 时向编译命令追加 -fsanitize 系列标志(详见下文"底层原理"小节)。

开启 AddressSanitizer(ASan)

$ ./config enable-asan
$ make

启用后,ASan 会在每次内存分配、释放、读写时进行边界与生命周期检查。该选项同时会顺带启用 LSan(LeakSanitizer),用于检测内存泄漏。

开启 UndefinedBehaviorSanitizer(UBSan)

$ ./config enable-ubsan
$ make

UBSan 会在运行时检查整数溢出、非法移位、空指针解引用、类型别名违规等未定义行为,并输出诊断信息。

开启 MemorySanitizer(MSan)

MSan 只由 Clang 实现,因此必须显式指定编译器为 clang:

$ CC=clang ./config enable-msan
$ make

关键前提:MSan 要求参与运行的所有代码(包括链接的库)都必须使用 MSan 编译,否则插桩缺失会导致误报或漏报。这使得 MSan 的使用难度明显高于 ASan 与 UBSan——不仅 OpenSSL 本身要开启,与之链接的第三方库同样需要以 MSan 模式构建。

组合使用:ASan + UBSan

ASan 与 UBSan 可同时启用:

$ ./config enable-asan enable-ubsan
$ make

这是开发中最常用的组合——一份构建同时覆盖内存错误与未定义行为两类问题。

互斥约束

  • ASan 与 MSan 不能同时使用,二者在编译器层面互斥;
  • 另需注意:Configure 在同时使用了 -rpath、共享库和任一 sanitizer 时会直接报错终止(die "***** Cannot simultaneously use -rpath, shared libraries, and any of asan, msan or ubsan",见 Configure),因为三者组合会破坏运行期链接的一致性。

从源码看 sanitizer 的底层实现

Configure 脚本对 sanitizer 的处理包含两个层面:

1. 命令行标志的自动注入。当对应选项未被禁用时,配置脚本会追加编译标志(见 Configure):

  • ASan:追加 -fsanitize=address(Windows/VC 目标为 /fsanitize=address);
  • UBSan:追加 -fsanitize=undefined-fno-sanitize-recover=all(遇错即停)以及 -DPEDANTIC;若编译器是 Clang,还会追加 -fno-sanitize=function(规避 Clang 对函数类型未定义行为的过度报告);
  • MSan:追加 -fsanitize=memory

2. 对已有 -fsanitize= 标志的自动识别。即使不通过 enable-* 选项,开发者自行在 CFLAGS 中传入 -fsanitize=address-fsanitize=undefined-fsanitize=memoryConfigure 也会通过正则解析 [-\/]fsanitize= 前缀自动检测,并将其从禁用列表中移除(见 Configure);若检测到"禁用某 sanitizer 却又在 CFLAGS 里显式开启"的矛盾配置,会直接报错。

此外,Configurations/shared-info.pl 中 Linux 共享库构建逻辑会检查 CFLAGS/cflags 中是否含 -fsanitize,若含则跳过 -Wl,-z,defs(未定义符号检查),避免 sanitizer 运行时库引入的符号导致链接失败(见 Configurations/shared-info.pl)。Configure 注释还指出 -DPEDANTIC 在 sanitizer 构建中是必须的,因为 sanitizer 本身同样会关注非标准行为,需要配合 -pedantic 保持一致性(见 Configure)。

一个值得注意的级联规则:MSan 启用时会自动禁用 asm(汇编代码),见 Configure 中的 sub { !$disabled{"msan"} } => [ "asm" ]。原因是汇编代码无法被 MSan 插桩,若保留会引入未初始化内存的漏报,因此 MSan 构建强制回退到纯 C 实现。

调试信息与栈回溯优化

当任一 sanitizer(或 fuzz 构建)开启时,Configure 会额外追加 -fno-omit-frame-pointer-g(非 VC 目标,见 Configure),保证错误报告中的栈回溯完整、可读。这意味着 sanitizer 构建天然附带调试符号,无需再手动加 -g

在 Sanitizer 构建下运行测试

构建完成后,直接用常规命令运行测试即可:

$ make test

Sanitizer 会在测试执行过程中自动检测问题,并把错误报告输出到 stderr;一旦检测到错误,对应的测试用例即判定为失败。

运行指定测试并输出详细日志

使用 make 变量 TESTS 指定要运行的测试,VERBOSE=1(等价 V=1)输出详细日志:

$ make test TESTS=test_name VERBOSE=1

TESTS 支持通配符、排除项、数字索引等丰富的表达方式(详见 test/README.md),例如:

$ make test TESTS='test_rsa test_dsa' VF=1                 # 指定多个测试并显示失败详情
$ make test TESTS='test_ssl* -test_ssl_*' test             # 通配与排除组合
$ make test TESTS='alltests -test_fuzz*' test              # 全部测试排除 fuzz 系列

其中 VERBOSEV)、VERBOSE_FAILUREVF)、VERBOSE_FAILURE_PROGRESSVFP)分别控制常规输出、失败详情输出与失败进度输出三个粒度。Windows 下对应为 nmake test,OpenVMS 下为 mms/macro="TESTS=..." test

Sanitizer 环境变量调优

Sanitizer 的运行时行为可以通过环境变量精确控制,这在处理已知误报、抑制泄漏噪音、或追求更详细诊断时非常实用。

ASAN_OPTIONS(控制 AddressSanitizer 行为)

# 允许 malloc 返回 NULL 而不是直接 abort(适合压力/失败注入场景)
ASAN_OPTIONS=allocator_may_return_null=1

# 关闭泄漏检测(LSan 默认作为 ASan 的一部分运行)
ASAN_OPTIONS=detect_leaks=0

# 获取更详细的栈回溯(禁用快速展开,代价是更慢)
ASAN_OPTIONS=fast_unwind_on_malloc=0

UBSAN_OPTIONS(控制 UndefinedBehaviorSanitizer 行为)

# 为 UBSan 错误打印栈回溯
UBSAN_OPTIONS=print_stacktrace=1

MSAN_OPTIONS(控制 MemorySanitizer 行为)

# 允许 malloc 返回 NULL 而不是直接 abort
MSAN_OPTIONS=allocator_may_return_null=1

组合示例:带环境变量的测试命令

$ ASAN_OPTIONS=detect_leaks=1 make test TESTS=test_name

一个实用的排查流程是:先用 make test 全量跑一遍,若有失败,再用 TESTS=<失败用例> VERBOSE=1 配合上述环境变量单独复现,缩小问题范围并获取完整报告。

解读 Sanitizer 报告

当 sanitizer 捕获到问题时,会输出包含以下信息的详细报告:

  • 错误类型:如 heap-buffer-overflow(堆缓冲区溢出)、use-after-free(释放后使用);
  • 栈回溯:出错位置(#0 起)的完整调用链;
  • 分配位置:对内存类错误,会给出该内存块最初被分配的位置;
  • 内存区域信息:涉及的地址、大小与所在区域(堆/栈/全局)。

典型 ASan 输出示例:

==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
    #0 0x... in function_name file.c:123
    #1 0x... in caller_function file.c:456
    ...

解读要点:

  1. 关注 #0 帧定位直接出错函数与行号;
  2. 若报告同时给出"allocated by"段,说明越界访问对象的创建位置,往往是修复的关键线索;
  3. 结合 -fno-omit-frame-pointer -g 构建(sanitizer 构建默认已启用),栈回溯中的符号与行号是可信的,可直接跳转到源码核对。

与 Valgrind 的对比与协同

特性 Sanitizers Valgrind
性能开销 约 2 倍减速 约 10~50 倍减速
是否需要重新编译 需要 不需要
内存泄漏检测 ASan(借助 LSan) 支持
未初始化内存检测 MSan 支持
缓冲区溢出检测 ASan 支持
未定义行为检测 UBSan 能力有限
平台支持 Linux、macOS Linux、macOS 等

两者各有适用场景:

  • 日常开发/CI 快速回归:优先 sanitizer 构建,速度快、覆盖 UBSan 这类 Valgrind 弱项;
  • 对无法重编译的依赖或最终产物做黑盒检查:使用 Valgrind,无需改动构建;
  • OpenSSL 项目的 Valgrind 实践:通过 EXE_SHELL 变量将测试包装在 util/wrap.pl valgrind --error-exitcode=1 --leak-check=full -q 下运行,并配合 OPENSSL_ia32cap=":0" 关闭超出 Valgrind 支持范围的 CPU 指令(见 NOTES-VALGRIND.md)。

此外值得注意的是:OpenSSL 4.0 起不再把 OPENSSL_cleanup() 注册为 atexit(3) 处理器,因此除非应用显式调用 OPENSSL_cleanup(),Valgrind 等泄漏检测工具可能把 still reachable 的内存块报告为泄漏,属预期行为而非真实泄漏(见 NOTES-VALGRIND.md)。这一背景同样适用于解读 sanitizer 构建中 LSan 的泄漏报告。

完整的实战工作流

将以上要点串成一套可直接落地的流程:

# 1. 开启 ASan + UBSan 组合构建(覆盖内存错误与未定义行为)
$ ./config enable-asan enable-ubsan
$ make

# 2. 全量测试,sanitizer 自动捕获问题并输出到 stderr
$ make test

# 3. 定位失败用例并复现,开启详细日志与栈回溯
$ ASAN_OPTIONS=fast_unwind_on_malloc=0 \
  UBSAN_OPTIONS=print_stacktrace=1 \
  make test TESTS=test_name VERBOSE=1

# 4. 需要检测未初始化内存时,改用 Clang + MSan(仅 Linux,需全链路 MSan 编译)
$ CC=clang ./config enable-msan
$ make test

# 5. 需要黑盒检查或无法重编译时,退回 Valgrind 方案
$ make test EXE_SHELL="$(/bin/pwd)/util/wrap.pl valgrind --error-exitcode=1 --leak-check=full -q" OPENSSL_ia32cap=":0"

总结

OpenSSL 将三种主流 sanitizer 深度集成进构建系统:enable-asanenable-ubsanenable-msan 三个配置开关背后,是 Configure-fsanitize 系列标志的自动注入、对冲突配置的拦截(如 ASan 与 MSan 互斥、-rpath 与共享库冲突)、对汇编代码的级联禁用(MSan 场景)以及对调试符号的自动增强。开发者只需掌握 ./config enable-*make test 这一核心流程,再辅以 ASAN_OPTIONS/UBSAN_OPTIONS/MSAN_OPTIONS 环境变量与 TESTS/VERBOSE 测试控制变量,即可在开发与 CI 中获得高效、可定位、可重复的运行时缺陷检测能力,与 Valgrind 形成完整互补。

延伸阅读

  • NOTES-VALGRIND.md —— 使用 Valgrind 运行测试的完整说明(EXE_SHELLOPENSSL_ia32capstill reachable 内存说明)
  • test/README.md —— 测试体系总览,涵盖 TESTS 变量语法、VERBOSE/VF/VFP 输出粒度等
  • INSTALL.md —— OpenSSL 整体编译安装说明(sanitizer 构建的基础)
  • Configure —— 配置脚本本体,sanitizer 标志注入与冲突检测逻辑所在
  • Configurations/shared-info.pl —— 共享库链接参数中对 -fsanitize 的特殊处理
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525