拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Infer:Pulse 内存安全分析器实战指南:Null 解引用检测、潜在问题与未知函数处理

静态分析代码质量开发工具【免费下载链接】inferA static analyzer for Java, C, C, and Objective-C项目地址https://gitcode.com/gh_mirrors/infer/infer点击查看免费下载Infer:Pulse 是开源静态分析工具 Infer 内置的跨过程interprocedural内存安全分析器用于检测 Java、C、C、Objective-C 等语言中的空指针解引用Null dereference、内存泄漏、使用已释放内存等缺陷。本文以 Pulse 官方文档 为主线结合仓库源码与测试用例完整讲解 Pulse 的核心机制——包括「仅在错误路径所有条件恒为真时才报告」的判定原则、潜在问题latent issues的延迟报告模型、对未知函数的乐观假设处理以及 Pulse 与 Nullsafe 注解检查器的协作方式。读完本文你将掌握如何用一条命令为 Java/C 项目开启 Pulse 分析、如何解读报告并利用infer debug检查被跳过的调用从而在真实项目中准确落地这一内存安全分析能力。Pulse 是什么跨过程的内存安全分析Pulse 是 Infer 中的一个分析检查器checker其定位在文档中定义得非常明确一个跨过程的内存安全分析interprocedural memory safety analysis。所谓跨过程意味着分析不会停留在单个函数内部而是会沿着调用链追踪指针、对象与内存状态这正是它能发现普通单函数分析难以捕获的深层缺陷的原因。在仓库的检查器注册代码 registerCheckers.ml 中可以看到Pulse 被注册为interprocedural_with_specialization带特化的跨过程分析并同时接入以下前端语言ClangC / C / Objective-CErlangHackJavaCIL.NET 中间语言PythonRustSwift从源码目录 infer/src/pulse 的结构看Pulse 的实现核心是一套「溯因abductive抽象域」其中 PulseAbductiveDomain.ml 定义了抽象状态PulseInterproc.ml 负责跨过程摘要summary的生成与组合PulseSummary.ml 则保存每个函数分析得到的摘要。可以推断Pulse 采用下近似under-approximate、析取disjunctive推理——--pulse-over-approximate-reasoning选项的帮助文本中就明确提到这一点见 Config.ml这意味着它只报告那些无论输入如何都必然发生的错误路径从而保证报告的高可信度。判定原则只在错误路径条件恒为真时报告文档强调了一个关键原则Errors are only reported when all conditions on the erroneous path are true regardless of input.即只有当错误路径上的所有条件与输入无关、恒为真时Pulse 才报告缺陷。这使 Pulse 的报告具有非常低的误报率——它宁可漏报false negative也不愿产生大量不可信的噪音报告。快速上手用 Pulse 分析 Java 空指针对 Java 项目运行 Pulse 只需一条命令文档原命令infer run --pulse -- javac Test.java--pulse即启用 Pulse 检查器若只想运行 Pulse 而关闭其他检查器可使用--pulse-only文档调试示例中即使用了该写法infer --pulse-only -- clang -c unknown_code.c。以下是用 Pulse 检测 Java 空指针解引用的完整示例来自文档class Person { Person emergencyContact; String address; Person getEmergencyContact() { return this.emergencyContact; } } class Registry { void create() { Person p new Person(); Person c p.getEmergencyContact(); // Null dereference here System.out.println(c.address); } void printContact(Person p) { // No null dereference, as we dont know anything about p System.out.println(p.getEmergencyContact().address); } }在这个例子中create()方法中p刚被new出来其字段emergencyContact尚未赋值因此c p.getEmergencyContact()必然得到null随后访问c.address就是必然发生的空指针解引用——Pulse 会在此报告NULLPTR_DEREFERENCE缺陷该问题类型详见 all-issue-types.md。printContact(Person p)中调用方传入的p是否为空完全未知p.getEmergencyContact()的返回值也无法确定因此 Pulse不报告——避免误报。但正因为分析是跨过程的一旦 Pulse 在某个调用点检测到p确实为null它会在printContact(p)的调用处报告空指针解引用。仓库中可找到大量针对这一行为的回归测试例如 infer/tests/codetoanalyze/c/pulse 目录下的 59 个 C 语言测试含abduce.c、aliasing.c、dangling_deref.c等覆盖别名、悬垂解引用、断言失败等各类内存安全场景。潜在问题Latent Issues延迟报告模型「潜在问题」是 Pulse 最核心、也最具特色的机制之一。其定义如下当错误只有在当前函数参数的部分取值下才会发生时Pulse 不立即报告而将其记为潜在latent问题一旦在某个调用点看到满足全部错误条件的调用该问题就变为**显式manifest**并被报告。这一机制在源码中有直接对应PulseLatentIssue.mli 的注释明确写道——latent issue 指代码中存在潜在问题但我们要延迟报告直到在某个调用上下文中看到缺陷条件真正显现。该模块还提供了should_report函数用于在调用点判断延迟报告还是立即报告。文档用一段 C 代码完整演示了 latent issue 从产生到显式化的全过程// for more realism, imagine that this function does other things as well void set_to_null_if_positive(int n, int** p) { if (n 0) { *p NULL; } } void latent_null_dereference(int n, int* p) { set_to_null_if_positive(n, p); *p 42; // NULL dereference! but only if n 0 so no report yet } void manifest_error(int *p) { // no way to avoid the bug here Pulse reports an error latent_null_dereference(1, p); }理解这段代码的关键latent_null_dereference中*p 42的解引用只有在n 0时才会空指针。由于n是参数条件取决于调用方因此此时不报告仅生成 latent issue。manifest_error以常量1调用latent_null_dereference此时n 0恒成立错误不可避免——Pulse 在这里报告显式错误。这个先记潜在、后在调用点判定的模型正是 Pulse 能兼顾低误报率与跨过程深度追踪的根本原因。未知函数Unknown Functions乐观假设与状态打乱在真实项目中分析器经常遇到找不到源码的函数——例如第三方库只暴露了签名或通过函数指针调用而无法解析到具体实现。为了控制误报Pulse 对未知函数调用采取乐观假设Pulse 会打乱scramble从调用参数可达的那部分抽象状态。所谓打乱可以理解为Pulse 主动遗忘这些指针对象的精确属性如已分配非空代之以不确定值。这样做通常能避免误报但文档也诚实指出它可能同时造成假阴性和假阳性并用一个经典例子说明void unknown(int* p); // third-party code that does [*p 5] // Infer doesnt have access to that code void false_negative() { int* x (int*) malloc(sizeof(int)); if (x) { // unknown call to x makes Pulse forget that x was allocated, in case it frees x unknown(x); } } // no memory leak reported: false negative! void false_positive(int *x) { unknown(x); // this sets *x to 5 if (x ! 5) { // unreachable int* p NULL; *p 42; // false positive reported here } }假阴性unknown(x)之后 Pulse 遗忘x已分配的事实以防unknown内部释放了x于是false_negative中的内存泄漏未被报告。假阳性unknown(x)实际把*x写成5但 Pulse 对x指向的值一无所知导致x ! 5分支被当作可达从而在不可达代码里报告了一个不真实的空指针。如何检查函数是否调用了未知函数文档给出了利用 Pulse 摘要summary排查未知函数调用的完整命令序列$ infer --pulse-only -- clang -c unknown_code.c No issues found $ infer debug --procedures --procedures-filter false_negative --procedures-summary ... skipped_calls{ unknown - call to skipped function occurs here }输出中的skipped_calls字段列出该函数摘要中所有被跳过的调用。这里的实现证据在 PulseSkippedCalls.ml它以AbstractDomain.Map (Procname) (SkippedTrace)形式维护一个从被跳过过程名到调用痕迹的映射痕迹的即时打印文本正是call to skipped function occurs here见 PulseSkippedCalls.ml。因此当你看到某个函数摘要中skipped_calls包含目标函数时即可确认 Pulse 对该函数采用了乐观假设、其结果可能不完全精确。Pulse × Nullsafe与注解检查器的协同Nullsafe 是 Infer 生态中针对 Java 的Nullable注解类型检查器遵循 Nullsafe 纪律的类需标注Nullsafe从而在类型层面声明其可空性契约。Pulse 与 Nullsafe 的协作点在于即使类已通过 Nullsafe 检查仍可能存在依赖方误用返回值的运行时空指针这正是 Pulse 的用武之地。沿用前文的Person/Registry例子若Person标注了Nullsafe同时把getEmergencyContact()显式标注为Nullable以表明该方法可能返回nullNullsafe(Nullsafe.Mode.LOCAL) class Person { Person emergencyContact; String address; Nullable Person getEmergencyContact() { return this.emergencyContact; } } class Registry { ... // Pulse reports here }依赖Person的Registry若未做空值处理Pulse 仍会报告空指针——因为Nullable声明只是类型层面的约定无法阻止运行时空指针而 Pulse 是真正追踪运行时值的分析器。控制 Nullsafe 文件上的 NPE 报告Pulse 是否在Nullsafe文件上报告空指针由--pulse-nullsafe-report-npe选项控制。其定义位于 Config.mland pulse_nullsafe_report_npe CLOpt.mk_bool ~long:pulse-nullsafe-report-npe ~default:true ~in_help:InferCommand.[(Analyze, manual_pulse)] Report null dereference issues on files marked Nullsafe.当前开源仓库的默认值为true即默认会在标注Nullsafe的文件上报告空指针解引用。文档中Facebook-specific: Pulse does not report onNullsafefiles一句则提示在 Facebook 内部部署版本中该默认行为有所不同默认不报告这是开源版与内部版的差异点使用时应以实际版本行为为准。此外还有一个配套选项--pulse-nullsafe-report-npe-as-separate-issue默认false见 Config.ml开启后Nullsafe文件上的空指针将作为独立的NULLPTR_DEREFERENCE_IN_NULLSAFE_CLASS问题类型单独报告便于在报告系统中区分普通 NPE与Nullsafe 类中的 NPE从而分别治理。进阶Pulse 常用配置选项速览围绕 Pulse 分析仓库在 Config.ml 中提供了丰富的命令行选项以下是与日常使用最相关的几类均为infer analyze下的--pulse-*参数选项默认值作用依据 Config.ml 帮助文本--pulse-nullsafe-report-npetrue是否在标注Nullsafe的文件上报告空指针--pulse-nullsafe-report-npe-as-separate-issuefalse是否将 Nullsafe 类中的 NPE 以独立的NULLPTR_DEREFERENCE_IN_NULLSAFE_CLASS问题类型报告--pulse-recency-limit32对同一地址最多跟踪的数组元素与结构体字段数量L2742-L2745--pulse-report-assertfalse对 C 代码中失败的断言报告PULSE_ASSERTION_ERRORL2748-L2751--pulse-model-skip-pattern无正则匹配应被建模为 skip跳过的方法L2616-L2619--pulse-model-return-nonnull/--pulse-model-return-nullable无正则匹配应建模为返回非空 / 可空的方法L2579-L2600--pulse-model-return-first-arg无正则匹配应建模为返回第一个参数的方法Java / C / Objective-CL2572-L2576--pulse-model-unknown-pure无正则匹配应建模为未知但纯函数的方法L2628-L2631其中--pulse-model-*系列允许在不改代码的前提下为第三方库方法提供语义模型如把free类方法建模为内存释放、把 getter 建模为返回首参从而显著提升对第三方代码的追踪精度。这些正则匹配的模型最终会汇入 PulseModels.ml 及各语言模型模块如 PulseModelsC.ml、PulseModelsJava.ml。总结Pulse 的设计哲学与实践要点回顾全文Pulse 的核心设计可以浓缩为三条原则这也是你在实际使用中最需要理解的三个要点只为必然路径发声只有当错误路径的所有条件与输入无关、恒为真时才报告这是低误报率的根基潜在问题延迟显式化跨过程摘要中保存 latent issue在调用点条件满足时升级为显式报告——PulseLatentIssue模块与should_report判定是这一能力的源码载体对未知保持乐观但可观测对未知函数打乱可达状态以避免误报同时通过infer debug --procedures --procedures-summary输出的skipped_calls字段让分析假设透明可查。实践中建议按以下流程使用先用infer run --pulse -- 编译命令跑出基线报告对存疑的未报告路径用infer debug --procedures --procedures-filter 函数名 --procedures-summary检查其摘要中是否存在skipped_calls若第三方库函数干扰严重再通过--pulse-model-*系列选项补充语义模型。这样即可在真实项目中充分发挥 Pulse 的跨过程内存安全分析能力。赞分享静态分析代码质量开发工具【免费下载链接】inferA static analyzer for Java, C, C, and Objective-C项目地址https://gitcode.com/gh_mirrors/infer/infer点击查看免费下载相关推荐ESP-IDF项目中WPA3认证处理函数的潜在内存安全问题分析ESP IDF项目中WPA3认证处理函数的潜在内存安全问题分析 在ESP IDF项目的无线安全协议实现中wpa3_process_rx_commit函数存在一物联网嵌入式创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门