- 静态分析
- 代码质量
- 开发工具
【免费下载链接】infer
A static analyzer for Java, C, C++, and Objective-C
导读
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 } }这里有两处问题:
c.y++:c是一个未初始化的结构体局部变量,只给x赋了值,y仍是未定义值,对它执行自增(先读后写)属于读取未初始化值;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} ... endValue:普通未初始化值,正是本文主题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 )这里有两个细节值得注意:
- 报错之后立即
initialize:注释写明"do not report further uninitialized reads errors on this value",即同一符号地址在后续路径中不再重复报告,避免刷屏; - 检查实现本身非常简单(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、_.f1、x.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_bad(int x; x = x;)是一个有趣的边界案例:x = x的右侧读取发生在赋值生效之前,因此仍然被判定为未初始化读取——这与 C 标准的语义一致(自赋值读取的是旧值)。
跨过程分析:未初始化状态如何跨越函数边界
Pulse 的未初始化检查不局限于单函数。它通过前置条件中的MustBeInitialized属性实现跨过程传播,机制如下:
- 被分析函数内:当
check_initialized发现某地址已初始化(没有Uninitialized属性)但还没有被写入过时,会向调用者"吸收"(abduce)一个MustBeInitialized前置条件(infer/src/pulse/PulseAbductiveDomain.ml#L507-L515),意思是"调用我时,这个实参对应的对象必须已初始化"; - 调用点:
check_all_valid收集被调函数前置条件中所有MustBeInitialized要求(infer/src/pulse/PulseInterproc.ml#L1161-L1197),按时间戳排序(保证报告最早的问题),然后用AddressAttributes.check_initialized在调用者的抽象状态中逐一检查实参地址(infer/src/pulse/PulseInterproc.ml#L1235-L1243),不满足即报告ReadUninitialized; - 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_good中int 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_ok | memset(&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 pair,pair.y未赋值就被整体返回,Pulse 通过字段级属性追踪在return处或调用方读取处报告。这一能力的实现依赖fold_pointer_targets递归遍历类型字段并为每个字段地址独立设置Uninitialized属性。
数组、动态内存与未知值
Pulse 对数组与动态内存的未初始化追踪同样细致:
- 堆内存:
malloc_bad(int* 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_FN与malloc_array_bad_FN都带_FN(false negative)后缀,即"当前不报告但语义上是错误",说明数组元素级的未初始化追踪尚未完全实现(见 uninit.c#L123-L142)。 - 未知函数返回值:从未知函数返回的结构体被认为是"任意值"(havoc),读取其字段不报告。
havoc_calling_unknown_struct_good(struct 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() {}不初始化成员x,uninit_user_provided_bad读取a.x被报告(uninit.cpp#L230-L248)。 - 函数内部分支都初始化则安全:
get_MyClass_infeasible_default的 switch 全部分支都返回,call_get_MyClass_infeasible_default_ok不报告;而get_MyClass_feasible_default的default分支没有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::Optional、folly::small_vector、std::__wrap_iter、std::atomic、std::function、std::optional、std::vector。这些容器/工具类型内部成员常被库代码以特殊方式初始化,直接打未初始化属性会带来大量误报(测试文件folly_expected.cpp、folly_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_bad(int* p; return *p;)与self_assign_bad(int x; x = x;)应产生两条PULSE_UNINITIALIZED_VALUE,与基线文件 infer/tests/codetoanalyze/c/pulse/issues.exp 中的期望输出一致。基线文件中的 trace 结构(variable ... declared here→read to uninitialized value occurs here)正是前文所述的诊断信息形态。
适用前提:上述命令需要在已经编译好的 Infer 环境中执行(参见仓库根目录 INSTALL.md 的构建指引);Pulse 需要 clang 前端的支持,
--pulse-only会跳过其他检查器只输出 Pulse 报告。
修复建议与总结
回到官方文档给出的核心建议:声明时即初始化所有变量。将文档中的示例修复为:
struct coordinates c = {0, 0}; int z = 0;结合本仓库的实践,更完整的修复清单如下:
- 局部变量声明时赋初值:
int x = 0;,而不是int x;后依赖后续分支赋值——uninit_if_zero_bad(uninit.c#L177-L185)展示了条件分支中只有部分路径赋值仍会被报告的场景; - 结构体整体初始化:使用
= {0}、= {}(C++ 值初始化)或memset(&s, 0, sizeof(s)),避免逐字段初始化遗漏(init_only_x_bad的教训); - 堆内存写入后读取:
malloc后先赋值再使用,或用calloc获取零初始化内存(malloc_badvsmalloc_good); - 检查"把未初始化对象传出函数"的路径:
init_only_x_bad表明即使在被调函数内部不读取,把未初始化字段整体返回/传出也会在调用方被捕获; - 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
相关推荐
Infer Pulse 检查器 PULSE_UNINITIALIZED_CONST:检测 Hack 抽象常量未初始化读取
Infer Pulse 检查器 PULSE_UNINITIALIZED_CONST:检测 Hack 抽象常量未初始化读取 PULSE_UNINITIALIZED
静态分析代码质量开发工具Infer 的 STACK_VARIABLE_ADDRESS_ESCAPE 检查:Pulse 如何捕获返回局部变量地址的悬垂指针
Infer 的 STACK_VARIABLE_ADDRESS_ESCAPE 检查:Pulse 如何捕获返回局部变量地址的悬垂指针 导读 本文深入讲解 Faceb
静态分析代码质量开发工具Infer 静态分析器 SIOF 检测实战:如何捕获 C++ 静态初始化顺序陷阱(STATIC_INITIALIZATION_ORDER_FIASCO)
Infer 静态分析器 SIOF 检测实战:如何捕获 C++ 静态初始化顺序陷阱(STATIC_INITIALIZATION_ORDER_FIASCO) 导读
静态分析代码质量开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考