ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

Infer 的 PULSE_UNINITIALIZED_VALUE 检查:Pulse 符号执行如何捕获未初始化值读取 静态分析代码质量开发工具【免费下载链接】inferA 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-L972let 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.yc是一个未初始化的结构体局部变量只给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每个值都有一个唯一的符号地址挂上一组属性attributesUninitialized就是其中之一。属性的定义在 infer/src/pulse/PulseAttribute.ml#L218-L266其中与本文相关的变体为| Uninitialized of UninitializedTyp.t而UninitializedTyp细分为三种子类型infer/src/pulse/PulseAttribute.ml#L173-L187module 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-L550let 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属性并打上Initializedinfer/src/pulse/PulseBaseAddressAttributes.ml#L241-L242let 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_initializedinfer/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 doesnt 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-L266codetoanalyze/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属性实现跨过程传播机制如下被分析函数内当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不满足即报告ReadUninitializedtrace 中会串联出完整的调用链例如测试基线中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_okmemset(pair, \0, sizeof(pair))后读取否memset_wrapper_init_ok通过包装函数set_pair_to_zero做 memset否memset相关用例非常能说明 Pulse 的建模精度memset被建模为逐字节初始化所以无论是直接调用还是经包装函数间接调用set_pair_to_zeroPulse 都能识别出结构体已被初始化。这也解释了为什么用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 hereissues.exp#L255而malloc_good分配后写入*p 5再读不报告。局部数组local_array_good写o[0]读o[0]不报告但逐个元素初始化时未写到的元素目前属于已知缺口——local_array_bad_FN与malloc_array_bad_FN都带_FNfalse 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_expPulse 不会误报call_init_by_conditionaql_exp_ok通过验证uninit.c#L215-L226。C 特性默认初始化、用户构造与黑名单C 场景在 uninit.cpp 中有专门覆盖值初始化value-initializationUninitDefault 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_default的default分支没有return构造了部分初始化的对象call_get_MyClass_feasible_default_bad被报告issues.exp#L429-L435。std::atomic 写操作建模init_by_store通过reinterpret_caststd::atomicMyClass*(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#L375trace 为 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 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_baduninit.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 与单字段结构体过滤等机制控制误报。理解了这套机制你不仅能修复报告出的问题也能在写代码时从源头规避未初始化值让程序行为真正可预测。赞分享静态分析代码质量开发工具【免费下载链接】inferA static analyzer for Java, C, C, and Objective-C项目地址https://gitcode.com/gh_mirrors/infer/infer点击查看免费下载相关推荐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_FIASCOInfer 静态分析器 SIOF 检测实战如何捕获 C 静态初始化顺序陷阱STATIC_INITIALIZATION_ORDER_FIASCO 导读静态分析代码质量开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表