news 2026/9/23 3:53:42

Infer 的 PULSE_UNINITIALIZED_VALUE 检查:Pulse 符号执行如何捕获未初始化值读取

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Infer 的 PULSE_UNINITIALIZED_VALUE 检查:Pulse 符号执行如何捕获未初始化值读取
  • 静态分析
  • 代码质量
  • 开发工具

【免费下载链接】infer

A static analyzer for Java, C, C++, and Objective-C

项目地址:https://gitcode.com/gh_mirrors/infer/infer
点击查看免费下载

导读

PULSE_UNINITIALIZED_VALUE是 Infer 静态分析器中 Pulse 检查器(内存安全分析器)报告的一类 Error 级缺陷:代码读取了一个尚未初始化的变量,导致结果不可预测甚至触发未定义行为(undefined behavior)。本文以 Infer 仓库中该问题类型的官方文档 infer/documentation/issues/PULSE_UNINITIALIZED_VALUE.md 为骨架,结合 Pulse 的源码实现(属性系统、诊断生成、跨过程校验)与测试用例(uninit.c、uninit.cpp、uninit.m)展开讲解:读完你不仅能准确识别这类缺陷、理解其危害,还能掌握 Pulse 追踪未初始化值的底层机制与修复方法。

问题定义:什么是未初始化值读取

官方文档对PULSE_UNINITIALIZED_VALUE的定义非常直接:

代码使用了一个尚未初始化的变量,导致结果不可预测或与预期不符("The code uses a variable that has not been initialized, leading to unpredictable or unintended results.")。

使用未初始化值可能导致未定义行为,具体表现为:程序崩溃、安全漏洞(例如将栈上残留的敏感数据作为逻辑值参与运算)以及错误结果(文档原文)。这类问题在 C/C++ 这类允许"声明即分配栈空间但不赋值"的语言中尤其常见;Java 等语言中局部变量也要求先赋值再使用,但结构体字段、指针指向的内存等场景仍可能出现类似问题。

该问题类型在 Infer 中以Error级别报告,属于MemoryError类别,由 Pulse 检查器产出。注册代码位于 infer/src/base/IssueType.ml#L969-L972:

let uninitialized_value_pulse = register ~category:MemoryError ~id:"PULSE_UNINITIALIZED_VALUE" Error Pulse ~hum:"Uninitialized Value" ~user_documentation:[%blob "./documentation/issues/PULSE_UNINITIALIZED_VALUE.md"]

注意[%blob ...]这一行:它把 infer/documentation/issues/PULSE_UNINITIALIZED_VALUE.md 的内容直接内嵌进二进制产物,用户在infer explain等交互中即可看到该文档,无需联网。在 website/docs/checker-pulse.md 的 Pulse 问题类型清单中也能找到PULSE_UNINITIALIZED_VALUE的条目。

官方文档示例(C 语言)

官方文档给出了一个非常典型的 C 示例:

struct coordinates { int x; int y; }; void foo() { struct coordinates c; c.x = 42; c.y++; // uninitialized value c.y! int z; if (z == 0) { // uninitialized value z! // something } }

这里有两处问题:

  1. c.y++c是一个未初始化的结构体局部变量,只给x赋了值,y仍是未定义值,对它执行自增(先读后写)属于读取未初始化值;
  2. if (z == 0):局部变量z声明后从未赋值就被读取参与条件判断。

官方文档同时给出了修复建议:"声明所有变量时即赋初始值"("This can easily be fixed by assigning all variables to an initial value when declaring them.")。例如将上述代码改为:

struct coordinates c = {0, 0}; // 或逐字段初始化 int z = 0;

触发原理:Pulse 如何"记住"一个值未初始化

Pulse 是基于符号执行的分离逻辑分析器。它把每个抽象值(AbstractValue,每个值都有一个唯一的符号地址)挂上一组属性(attributes)Uninitialized就是其中之一。属性的定义在 infer/src/pulse/PulseAttribute.ml#L218-L266,其中与本文相关的变体为:

| Uninitialized of UninitializedTyp.t

UninitializedTyp细分为三种子类型(infer/src/pulse/PulseAttribute.ml#L173-L187):

module UninitializedTyp = struct type t = | Value | Const of Fieldname.t | DictMissingKey of {dict: DecompilerExpr.t; key: Fieldname.t} ... end
  • Value:普通未初始化值,正是本文主题PULSE_UNINITIALIZED_VALUE对应的情形;
  • Const:未初始化的类常量,对应PULSE_UNINITIALIZED_CONST(如 Hack 语言中类静态常量在类初始化前被读取);
  • DictMissingKey:字典(dict)中读取不存在的键,对应PULSE_DICT_MISSING_KEY

三种子类型共用同一个ReadUninitialized诊断,最终由 infer/src/pulse/PulseDiagnostic.ml#L1314-L1319 的映射函数分发到不同的 IssueType:

| ReadUninitialized {typ= Value}, _ -> IssueType.uninitialized_value_pulse | ReadUninitialized {typ= Const _}, _ -> IssueType.pulse_uninitialized_const | ReadUninitialized {typ= DictMissingKey _}, _ -> IssueType.pulse_dict_missing_key

设置与清除:属性生命周期

设置:当 Pulse 遇到局部变量声明时,会调用realloc_pvar把该变量的地址标记为未初始化(infer/src/pulse/PulseOperations.ml#L543-L550):

let realloc_pvar tenv ({PathContext.timestamp} as path) ~set_uninitialized pvar typ location astate = ... if set_uninitialized then AddressAttributes.set_uninitialized tenv path ...

值得注意的是~set_uninitialized这个开关:在 infer/src/pulse/Pulse.ml#L1545 中可以看到,C++ 结构化绑定(structured binding)和folly::coro类型不会被标记为未初始化,以避免误报。标记过程的实现位于 infer/src/pulse/PulseAbductiveDomain.ml#L1630-L1648:它会递归遍历类型的字段(fold_pointer_targets),对每个可寻址的字段地址打上Uninitialized Value属性,同时应用两条重要过滤规则:

  • 忽略 union 类型字段CUnion _ | CppClass {is_union= true}),因为 union 的"未初始化"语义与普通结构体不同;
  • 忽略单字段结构体filter_struct{fields= [_]}返回 false),这是为了规避已知误报(源码注释引用 D26146578);
  • 忽略黑名单类型,见下文。

malloc/calloc等动态分配也有专门处理:在 infer/src/pulse/PulseModelsImport.ml#L361-L401 中,alloc_not_null_common通过set_uninitialized把新分配且未initialize的堆内存标记为未初始化——这就是为什么"malloc 后未写入就读取"也会被报告。

清除:一旦发生写入,Pulse 通过initialize移除Uninitialized属性并打上Initialized(infer/src/pulse/PulseBaseAddressAttributes.ml#L241-L242):

let initialize address memory = add_one address Initialized memory |> remove_attribute Attributes.remove_uninitialized address

remove_uninitialized按属性优先级删除(infer/src/pulse/PulseAttribute.ml#L1049)。也就是说,同一个抽象地址在任意路径上被写入后,该路径上就不再携带未初始化属性,因此"先初始化再读取"的合法代码不会被报告。

读取时的检查:check_initialized

当 Pulse 符号执行到一次内存读取(access_mode = Read)时,会先调用AddressAttributes.check_initialized(infer/src/pulse/PulseOperations.ml#L35-L45):

| Read -> AddressAttributes.check_initialized path access_trace address astate |> Result.map_error ~f:(fun typ -> ReportableError { diagnostic= Diagnostic.ReadUninitialized {typ; calling_context= []; trace= access_trace} ; astate } ) |> AccessResult.of_result_f path ~f:(fun _ -> (* do not report further uninitialized reads errors on this value *) AddressAttributes.initialize address astate )

这里有两个细节值得注意:

  1. 报错之后立即initialize:注释写明"do not report further uninitialized reads errors on this value",即同一符号地址在后续路径中不再重复报告,避免刷屏;
  2. 检查实现本身非常简单(infer/src/pulse/PulseBaseAddressAttributes.ml#L176-L183):在属性图中查找该地址的Uninitialized属性,存在则返回Error typ,否则返回Ok ()

写入即合法:初始化的传播

写入操作(Write)会直接调用AddressAttributes.initialize,因此任何赋值(包括memset、通过指针写入、调用init_int_ref这类"按引用初始化"的函数)都会让 Pulse 认为该地址已初始化。这与 C 语言中"向指针解引用写入相当于初始化所指对象"的语义一致。

报告形态:诊断信息与访问路径

ReadUninitialized诊断不仅报告"发生了未初始化读取",还会在错误消息中给出可读的访问路径(access path),帮助开发者定位到具体是哪个变量/字段没初始化。实现位于 infer/src/pulse/PulseDiagnostic.ml#L817-L881:

  • 从 trace 中回溯VariableDeclared(局部变量声明)或FormalDeclared(形参声明)得到根变量名;
  • 回溯StructFieldAddressCreated得到字段路径;
  • 组合成如x_.f1x.g1.f1这样的可读路径;
  • 最终消息形如`%s` is read without initialization(普通值)或`%s` doesn't seem to be initialized. This will cause a runtime error(常量/字典键)。

错误 trace 中"read to uninitialized value occurs here"这一锚点由 infer/src/pulse/PulseDiagnostic.ml#L1196-L1201 生成。从测试基线可以看到真实报告输出,例如 infer/tests/codetoanalyze/c/pulse/issues.exp#L252-L266:

codetoanalyze/c/pulse/uninit.c, dereference_bad, 2, PULSE_UNINITIALIZED_VALUE, no_bucket, ERROR, [variable `p` declared here,read to uninitialized value occurs here] codetoanalyze/c/pulse/uninit.c, self_assign_bad, 2, PULSE_UNINITIALIZED_VALUE, no_bucket, ERROR, [variable `x` declared here,read to uninitialized value occurs here] codetoanalyze/c/pulse/uninit.c, malloc_bad, 3, PULSE_UNINITIALIZED_VALUE, no_bucket, ERROR, [allocated by call to `malloc` (modelled),assigned,read to uninitialized value occurs here]

self_assign_badint x; x = x;)是一个有趣的边界案例:x = x的右侧读取发生在赋值生效之前,因此仍然被判定为未初始化读取——这与 C 标准的语义一致(自赋值读取的是旧值)。

跨过程分析:未初始化状态如何跨越函数边界

Pulse 的未初始化检查不局限于单函数。它通过前置条件中的MustBeInitialized属性实现跨过程传播,机制如下:

  1. 被分析函数内:当check_initialized发现某地址已初始化(没有Uninitialized属性)但还没有被写入过时,会向调用者"吸收"(abduce)一个MustBeInitialized前置条件(infer/src/pulse/PulseAbductiveDomain.ml#L507-L515),意思是"调用我时,这个实参对应的对象必须已初始化";
  2. 调用点check_all_valid收集被调函数前置条件中所有MustBeInitialized要求(infer/src/pulse/PulseInterproc.ml#L1161-L1197),按时间戳排序(保证报告最早的问题),然后用AddressAttributes.check_initialized调用者的抽象状态中逐一检查实参地址(infer/src/pulse/PulseInterproc.ml#L1235-L1243),不满足即报告ReadUninitialized
  3. trace 中会串联出完整的调用链,例如测试基线中interprocedural_read_in_callee_bad的报告:
[variable `x` declared here,when calling `read_int_ref` here,parameter `p` of read_int_ref,read to uninitialized value occurs here]

(见 infer/tests/codetoanalyze/c/pulse/issues.exp#L257)。interprocedural_uninit_in_callee_bad则展示了更长的跨过程链:malloc分配 → 在uninit中返回 → 赋给p→ 解引用读取,共 3 跳。

"可能初始化"(may-initialize)的处理:如果被调函数在某些分支初始化、某些分支不初始化,Pulse 通过条件分支的符号执行自然区分。例如 uninit.c 中的init_ptr_zero:当i == 0时走else分支写*(ptr + i)(等价于写*ptr),因此call_init_ptr_zero_goodint y = x;是合法的(测试标记为_good不报告);而当形如use_and_mayinit(x, &x)这种"既传值又传地址、可能在内部初始化"的场景,Pulse 采取保守策略:call_to_use_and_mayinit_bad仍被报告(issues.exp#L254),因为实参作为值传递时读取的是未初始化值。

结构与字段的字段级追踪:不只盯局部变量

Pulse 对未初始化的追踪细化到字段粒度。结构体struct uninit_s { int f1; int f2; };中只初始化f1、随后读取f2,会被精确报告,相关测试覆盖如下(均见 uninit.c):

测试函数行为是否报告
interprocedural_struct_good调用init_f1(&s)后读s.f1
interprocedural_struct_bad调用init_f1(&s)后读s.f2
nested_struct_bad嵌套结构体x.g1.f2 = 42后读x.g1.f1
init_only_x_bad只初始化pair.x就返回整个结构体
memset_init_okmemset(&pair, '\0', sizeof(pair))后读取
memset_wrapper_init_ok通过包装函数set_pair_to_zero做 memset

memset相关用例非常能说明 Pulse 的建模精度:memset被建模为"逐字节初始化",所以无论是直接调用还是经包装函数间接调用(set_pair_to_zero),Pulse 都能识别出结构体已被初始化。这也解释了为什么"用memset清零结构体"是 C 代码中初始化结构体的推荐做法之一。

init_only_x_bad对应"返回部分初始化的结构体":pair.x = x之后直接return pairpair.y未赋值就被整体返回,Pulse 通过字段级属性追踪在return处或调用方读取处报告。这一能力的实现依赖fold_pointer_targets递归遍历类型字段并为每个字段地址独立设置Uninitialized属性。

数组、动态内存与未知值

Pulse 对数组与动态内存的未初始化追踪同样细致:

  • 堆内存malloc_badint* p = malloc(...); int x = *p;)被报告,trace 显示 "allocated by call tomalloc(modelled),assigned,read to uninitialized value occurs here"(issues.exp#L255);而malloc_good(分配后写入*p = 5再读)不报告。
  • 局部数组local_array_good(写o[0]o[0])不报告;但逐个元素初始化时,未写到的元素目前属于已知缺口——local_array_bad_FNmalloc_array_bad_FN都带_FN(false negative)后缀,即"当前不报告但语义上是错误",说明数组元素级的未初始化追踪尚未完全实现(见 uninit.c#L123-L142)。
  • 未知函数返回值:从未知函数返回的结构体被认为是"任意值"(havoc),读取其字段不报告。havoc_calling_unknown_struct_goodstruct uninit_s x = unknown_wrapper(); int y = x.f1;)验证了这一点(uninit.c#L114-L121)。
  • 全局变量:全局结构体(默认零初始化)作为条件表达式返回值时(init_by_conditional_exp),Pulse 不会误报,call_init_by_conditionaql_exp_ok通过验证(uninit.c#L215-L226)。

C++ 特性:默认初始化、用户构造与黑名单

C++ 场景在 uninit.cpp 中有专门覆盖:

  • 值初始化(value-initialization)UninitDefault a{};会按规则把a.x零初始化,因此uninit_default_ok不报告;而用户提供的默认构造函数UninitUserProvided() {}不初始化成员xuninit_user_provided_bad读取a.x被报告(uninit.cpp#L230-L248)。
  • 函数内部分支都初始化则安全get_MyClass_infeasible_default的 switch 全部分支都返回,call_get_MyClass_infeasible_default_ok不报告;而get_MyClass_feasible_defaultdefault分支没有return(构造了部分初始化的对象),call_get_MyClass_feasible_default_bad被报告(issues.exp#L429-L435)。
  • std::atomic 写操作建模init_by_store通过reinterpret_cast<std::atomic<MyClass>*>(x)->store(y, ...)写入被视为完整初始化,call_init_by_store_ok不报告。
  • 黑名单机制:为避免误报,Pulse 维护了一个"不检查未初始化"的类型黑名单(infer/src/pulse/PulseUninitBlocklist.ml),包括folly::Optionalfolly::small_vectorstd::__wrap_iterstd::atomicstd::functionstd::optionalstd::vector。这些容器/工具类型内部成员常被库代码以特殊方式初始化,直接打未初始化属性会带来大量误报(测试文件folly_expected.cppfolly_try.cpp中即记录了针对 folly 的误报修复)。

Objective-C 场景

Objective-C 的 struct 场景在 uninit.m 中有覆盖:Uninit.call_setter_c_struct_bad通过 setter 传入一个只初始化了部分字段的 C 结构体后被报告(issues.exp#L375),trace 为 "struct field addressxcreated, when callingUninit.setS:here, parametersof Uninit.setS:, read to uninitialized value occurs here"——说明未初始化追踪同样适用于 OC 方法调用参数传递。

如何运行与验证

Pulse 检查器通过--pulse--pulse-only激活(参考 website/docs/checker-pulse.md):

# 对单个 C 文件运行(clang 前端) infer run --pulse -- clang -c uninit.c # 仅运行 Pulse 检查器 infer --pulse-only -- clang -c unknown_code.c # 对 Java 项目 infer run --pulse -- javac Test.java

如果你克隆了 Infer 仓库,可以用仓库自带的测试用例快速验证行为。例如 infer/tests/codetoanalyze/c/pulse/uninit.c 中dereference_badint* p; return *p;)与self_assign_badint x; x = x;)应产生两条PULSE_UNINITIALIZED_VALUE,与基线文件 infer/tests/codetoanalyze/c/pulse/issues.exp 中的期望输出一致。基线文件中的 trace 结构(variable ... declared hereread to uninitialized value occurs here)正是前文所述的诊断信息形态。

适用前提:上述命令需要在已经编译好的 Infer 环境中执行(参见仓库根目录 INSTALL.md 的构建指引);Pulse 需要 clang 前端的支持,--pulse-only会跳过其他检查器只输出 Pulse 报告。

修复建议与总结

回到官方文档给出的核心建议:声明时即初始化所有变量。将文档中的示例修复为:

struct coordinates c = {0, 0}; int z = 0;

结合本仓库的实践,更完整的修复清单如下:

  1. 局部变量声明时赋初值int x = 0;,而不是int x;后依赖后续分支赋值——uninit_if_zero_bad(uninit.c#L177-L185)展示了条件分支中只有部分路径赋值仍会被报告的场景;
  2. 结构体整体初始化:使用= {0}= {}(C++ 值初始化)或memset(&s, 0, sizeof(s)),避免逐字段初始化遗漏(init_only_x_bad的教训);
  3. 堆内存写入后读取malloc后先赋值再使用,或用calloc获取零初始化内存(malloc_badvsmalloc_good);
  4. 检查"把未初始化对象传出函数"的路径init_only_x_bad表明即使在被调函数内部不读取,把未初始化字段整体返回/传出也会在调用方被捕获;
  5. C++ 中警惕用户提供的默认构造函数UninitUserProvided() {}不会初始化成员,改为= default+ 成员默认值,或使用值初始化{}

总结一下:PULSE_UNINITIALIZED_VALUE是 Infer Pulse 对"读取未初始化值"这一未定义行为的精确捕获。从实现上看,它依赖一套完整的属性(attribute)抽象——变量/字段/堆内存地址在声明或分配时被打上Uninitialized属性,写入时被清除,读取时被检查,并借助MustBeInitialized前置条件实现跨过程传播;从使用上看,它覆盖局部变量、结构体字段、嵌套结构体、堆内存、C++ 默认/值初始化、Objective-C 结构体参数等丰富场景,同时通过黑名单、union 与单字段结构体过滤等机制控制误报。理解了这套机制,你不仅能修复报告出的问题,也能在写代码时从源头规避未初始化值,让程序行为真正可预测。

  • 静态分析
  • 代码质量
  • 开发工具

【免费下载链接】infer

A static analyzer for Java, C, C++, and Objective-C

项目地址:https://gitcode.com/gh_mirrors/infer/infer
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 3:52:35

AI大模型Python实战V7.5:环境搭建、流式输出与本地部署全链路指南

1. 这套V7.5版本到底在解决什么问题先把话说在前头&#xff0c;这个标题里的“AI大模型Python线下V7.5版本”听起来像是一个课程或者训练营的版本号&#xff0c;但如果你真在一线做过大模型应用开发&#xff0c;就会明白它背后指向的是一套完整的、可落地的技术方案组合。它不是…

作者头像 李华
网站建设 2026/9/23 3:52:01

1:1模仿张一鸣阅读法:一年读完50本书的实操拆解与避坑指南

开头就直接上干货&#xff1a;我试着1:1模仿张一鸣的阅读习惯和信息管理方式&#xff0c;坚持了整整一年&#xff0c;实际读完了50本书&#xff0c;不是收藏夹吃灰的那种“读完”&#xff0c;是每本都做了笔记、每两周逼自己做一个行动实验的那种。这篇文章不聊鸡汤&#xff0c…

作者头像 李华
网站建设 2026/9/23 3:51:51

Java课设实战:电影管理系统从数据库设计到核心代码全解析

简介&#xff1a;一份基于Java的简易电影管理系统源码包&#xff0c;面向Java初学者、课程设计者或小型资料库管理者。系统整合Java后端、JSP动态页面、PHP接口以及JavaScript、CSS、HTML前端技术&#xff0c;提供电影信息录入、查询、管理、展示等完整功能&#xff0c;适合作为…

作者头像 李华
网站建设 2026/9/23 3:50:29

Starlette 开发脚本全指南:从安装、测试到发布的一体化工作流

Starlette 开发脚本全指南&#xff1a;从安装、测试到发布的一体化工作流 【免费下载链接】starlette The little ASGI framework that shines. &#x1f31f; 项目地址: https://gitcode.com/gh_mirrors/st/starlette 导读 本文聚焦 Starlette 仓库中 scripts/README.…

作者头像 李华
网站建设 2026/9/23 3:50:27

基于个人信息自动生成密码猜测字典的Python脚本

做安全测试的人多多少少都遇到过这种场景&#xff1a;手头有一批密文或者哈希&#xff0c;常规密码猜测字典跑完一轮&#xff0c;命中率惨不忍睹&#xff0c;转头想自己做一份专属字典&#xff0c;却不知道从哪里下手。网上的通用字典动辄几个GB&#xff0c;看着很唬人&#xf…

作者头像 李华
网站建设 2026/9/23 3:49:21

Blender新手启动障碍:从UI净化到坐标系直觉的零基础重构

1. 这不是又一套“点开就学”的Blender教程——它解决的是新手根本没意识到的启动障碍 你打开Blender&#xff0c;界面像一张密不透风的电路板&#xff1a;左上角一堆图标、右下角浮动面板、3D视图里悬浮着一个灰白立方体&#xff0c;鼠标滚轮缩放时视角突然卡顿&#xff0c;按…

作者头像 李华