Root cause 摘要:Linux 6.8(Ubuntu24.04 默认)
vm.mmap_rnd_bits=32和容器内 gcc‑12.2 libasan 不兼容。ASAN 在main()执行前初始化阶段随机触发段错误,ASAN 的信号处理器死循环打印AddressSanitizer:DEADLYSIGNAL,把 CPU 打满 100%。最小复现程序:int main(){return 0;}开启 ASAN 编译,约 1/3 概率复现卡死,栈全部落在 libasan/ldso(AsanInitInternal → SizeClassAllocator64::Init),业务代码完全还没执行,排除业务、流、信号业务逻辑问题。
1. 为什么 vm.mmap_rnd_bits=32 会造成这个现象
vm.mmap_rnd_bits 控制 mmap 虚拟地址的随机化比特位数,数值越大,地址空间ASLR随机偏移范围越大。
- 内核6.8+ Ubuntu24.04 默认:
vm.mmap_rnd_bits = 32,mmap 会给出跨度极大的64位虚拟地址偏移。 - ASAN 的
SizeClassAllocator64内存分配器初始化阶段,会预留一大块固定布局的虚拟地址区间用于影子内存(shadow memory)。
libasan‑12.2 的地址布局假设ASLR随机偏移不会过大 ;当内核给出极端高位的随机mmap地址,会和ASAN预留的影子内存地址范围发生虚拟地址冲突/重叠。
AsanInitInternal初始化64位分配器,调用 mmap 预留影子内存区域- 内核因
mmap_rnd_bits=32返回一个冲突的随机虚拟地址 - 内部触发 SIGSEGV(在 main 之前,业务代码尚未运行)
- ASAN注册的SIGSEGV信号处理器接管;但此时ASAN自身初始化还没完成,信号处理内部状态不完整
- 信号 handler 尝试打印 DEADLYSIGNAL,又触发访问错误,信号处理递归触发信号 → 死循环,CPU 100%。
关键点:不是普通崩溃退出,是信号handler无限循环,进程不退出,CPU跑满。最小空main程序就复现,证明和应用代码无关。
2. 现象特征(用于排查确认)
-
进程CPU持续100%,不崩溃、不core dump、不退出
-
stderr 持续刷屏:
AddressSanitizer:DEADLYSIGNAL -
gdb bt 全部在libasan内部:
#0 ... inside signal handler
#1 __libasan::AsanInitInternal
#2 __libasan::SizeClassAllocator64::Init
#3 ld‑linux.so startup, before main() -
复现是间歇性:约1/3概率,ASLR随机,不是每次必现。
-
仅触发条件组合:
- Host内核 ≥6.8,
vm.mmap_rnd_bits=32(Ubuntu24.04默认) - 容器内部 gcc‑12.2 libasan
即使容器是旧的Ubuntu/Debian,只要宿主机内核是6.8+,mmap的ASLR行为由宿主机内核决定!容器不能隔离
vm.mmap_rnd_bits,这是关键坑。 - Host内核 ≥6.8,
⚠️ 重要提醒:vm.mmap_rnd_bits 是宿主机内核sysctl,不是容器内可独立设置的参数。容器无法覆盖宿主机该内核行为,哪怕容器内部sysctl读出来别的值,实际mmap行为跟随host kernel。
3. 验证复现(最小case)
c
// main.c
int main(){return 0;}
编译:
bash
gcc -fsanitize=address main.c -o asan_demo
# 循环跑,大概1/3次卡死CPU100%
for i in {1..100}; do ./asan_demo; done
gdb attach 卡死进程,可以看到栈在asan初始化+信号处理循环,还没有进入main。
4. 修复方案(三种可选)
方案A:宿主机降低 vm.mmap_rnd_bits(根治,推荐测试环境)
把宿主机的该内核参数下调,例如设为 28,规避过大ASLR偏移触发asan地址冲突。
bash
# 临时生效 host
sysctl -w vm.mmap_rnd_bits=28
# 永久写入 /etc/sysctl.conf
echo "vm.mmap_rnd_bits = 28" >> /etc/sysctl.conf
sysctl -p
注意:必须改宿主机,容器内部sysctl修改该参数会报错无权限,不生效。
方案B:ASAN侧环境变量规避,不改动内核
ASAN 提供环境变量收紧虚拟地址随机,规避内核过大ASLR偏移:
bash
export ASAN_OPTIONS=mmap_randomize_shadow_memory=0
./your_asan_binary
关闭asan影子内存自身的mmap随机化,可以规避该冲突。适合CI容器场景,不需要修改宿主机sysctl。
方案C:升级libasan版本
gcc‑13+ / llvm‑16+ 的 libasan 修复了对高 mmap_rnd_bits 的兼容性,改进64位分配器地址布局逻辑,不再受32bit rnd bits影响。
容器内升级gcc到13+,替换libasan。
方案D:临时规避,关闭ASLR(不推荐生产,仅调试)
bash
setarch $(uname -m) -R ./asan_demo
5. 为什么这不是业务代码bug
- 故障发生在
main()执行之前,AsanInitInternal,应用代码一行都没跑; - 空main最小程序稳定复现;
- 触发条件完全依赖host内核
vm.mmap_rnd_bits=32和特定版本libasan组合; - 故障根因:libasan‑12.2 的
SizeClassAllocator64::Init没有处理内核返回极大ASLR随机偏移,初始化阶段SIGSEGV后信号处理器递归死循环。
6. 补充背景与相关内核/LLVM信息
Linux 6.8 调高了默认 vm.mmap_rnd_bits,提升安全熵;但是老版本ASAN 64位分配器对超大mmap随机偏移缺少健壮处理:
- ASAN 64bit模式需要预留大片连续虚拟地址空间用于shadow memory;
当内核返回的mmap随机偏移过高,会抢占asan需要的预留地址区间,初始化mmap调用失败,触发segfault;
初始化未完成时,asan信号handler没有做好保护,发生fault‑in‑signal‑handler,无限循环,CPU打满。
很多CI用Ubuntu24.04 host跑老版本gcc‑12容器,就会踩这个间歇性卡死bug,现象非常迷惑:测试随机挂、CPU100、看不到业务栈。