vm.mmap_rnd_bits 引发 ASAN CPU 100% 问题解析

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预留的影子内存地址范围发生虚拟地址冲突/重叠
  1. AsanInitInternal 初始化64位分配器,调用 mmap 预留影子内存区域
  2. 内核因 mmap_rnd_bits=32 返回一个冲突的随机虚拟地址
  3. 内部触发 SIGSEGV(在 main 之前,业务代码尚未运行)
  4. ASAN注册的SIGSEGV信号处理器接管;但此时ASAN自身初始化还没完成,信号处理内部状态不完整
  5. 信号 handler 尝试打印 DEADLYSIGNAL,又触发访问错误,信号处理递归触发信号 → 死循环,CPU 100%。

关键点:不是普通崩溃退出,是信号handler无限循环,进程不退出,CPU跑满。最小空main程序就复现,证明和应用代码无关。

2. 现象特征(用于排查确认)

  1. 进程CPU持续100%,不崩溃、不core dump、不退出

  2. stderr 持续刷屏:AddressSanitizer:DEADLYSIGNAL

  3. gdb bt 全部在libasan内部:

    #0 ... inside signal handler
    #1 __libasan::AsanInitInternal
    #2 __libasan::SizeClassAllocator64::Init
    #3 ld‑linux.so startup, before main()

  4. 复现是间歇性:约1/3概率,ASLR随机,不是每次必现。

  5. 仅触发条件组合:

    • Host内核 ≥6.8,vm.mmap_rnd_bits=32(Ubuntu24.04默认)
    • 容器内部 gcc‑12.2 libasan

    即使容器是旧的Ubuntu/Debian,只要宿主机内核是6.8+,mmap的ASLR行为由宿主机内核决定!容器不能隔离 vm.mmap_rnd_bits,这是关键坑。

⚠️ 重要提醒: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

  1. 故障发生在main()执行之前,AsanInitInternal,应用代码一行都没跑;
  2. 空main最小程序稳定复现;
  3. 触发条件完全依赖host内核vm.mmap_rnd_bits=32和特定版本libasan组合;
  4. 故障根因: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、看不到业务栈。

相关推荐
码农小韩3 小时前
Linux应用开发(六)——进程间通信
linux·linux驱动·嵌入式软件开发·嵌入式操作系统·linux应用
小祺先生4 小时前
mt7921 debian系统 如何安装驱动
运维·网络·debian
笨笨饿4 小时前
#138_解决Codex要五次回复的问题
linux·stm32·单片机·嵌入式硬件·mcu·物联网·嵌入式实时数据库
m0_459872924 小时前
Linux 系统基础
linux·运维·服务器
吴声子夜歌4 小时前
Shell编程——数组
linux·运维·shell
qq_508576095 小时前
机器人通讯协议
网络·机器人
2401_891957315 小时前
简单了解多路转接select
运维·服务器
blueSatchel5 小时前
ros-humble仿真-建图-导航
linux·ros2
Lucis__5 小时前
基于责任链模式的消息队列—异步处理流水线的最佳实践
linux·c++·消息队列·责任链模式·ipc
程序员-Benothing5 小时前
Shell 脚本条件判断与流程控制:if for while case 详解
linux·运维·服务器