Facebook 的开源静态分析工具Infer


开始使用

依赖配置

https://github.com/facebook/infer/blob/master/INSTALL.md#how-to-compile-and-install-infer

下载

https://github.com/facebook/infer/releases/tag/v0.7.0

安装

在命令行,到下载目录,解压:

tar xf infer-*-v0.7.0.tar.xz

cd infer-*-v0.7.0./build-infer.sh

exportPATH=`pwd`/infer/bin:$PATH

Mac 下执行脚本报错找不到opam

需要使用brew 安装下(如果没有安装brew,请执行:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

使用brew 安装opam及相关依赖,请执行:

brew install autoconf automake opam caskroom/cask/brew-cask

你可用通过在命令行执行 echo $SHELL 确定所使用的 shell。根据具体的情况调整上面的命令。

如果你是在 linux 中,如果 ~/.bash_profile 不存在的话,你也许需要把 ~/.bash_profile 替换成 ~/.bashrc

Hello, World!

根据本页的说明,你可以使用 Infer 尝试检查一些简单的例子。你会看到 Infer 报告了一些问题,修复这些问题,再进行一次检测,Infer 便不再报告问题。这将使得我们对 Infer 如何工作有一些初步的认识,更近一步的使用,可以参考用户使用参考

以下所有的例子都可以在 infer/examples 中找到。

Hello world Java

以下是一个简答的 Java 例子。

// Hello.java
class Hello {
  int test() {
    String s = null;
    return s.length();
  }
}

通过以下命令在 Hello.java 同级目录执行 Infer。

infer -- javac Hello.java

你将会看到以下的报告输出:

Hello.java:5: error: NULL_DEREFERENCE
  object s last assigned on line 4 could be null and is dereferenced at line 5  

编辑文件,加入为空检查:

  int test() {
    String s = null;
    return s == null ? 0 : s.length();
  }

再次运行 Infer,这次,运行结果显示 No issues found,未发现错误。

Hello world Objective-C

以下是一个简单的 Objective-C 例子:

// Hello.m
#import <Foundation/Foundation.h>

@interface Hello: NSObject
@property NSString* s;
@end

@implementation Hello
NSString* m() {
    Hello* hello = nil;
    return hello->_s;
}
@end

在 Hello.m 同级目录,运行:

infer -- clang -c Hello.m

以下是错误报告输出:

Hello.m:10 NULL_DEREFERENCE
  pointer hello last assigned on line 9 could be null and is dereferenced at line 10, column 12

编辑,如下修正:

NSString* m() {
    Hello* hello = nil;
    return hello.s;
}

再次运行,No issues found, 没有报错。

Hello world C

一个简单的 C 例子:

// hello.c
#include <stdlib.h>

void test() {
  int *s = NULL;
  *s = 42;
}

在 hello.c 同级目录运行:

infer -- gcc -c hello.c

报错输出:

hello.c:5: error: NULL_DEREFERENCE
  pointer s last assigned on line 4 could be null and is dereferenced at line 5, column 10

编辑,修正:

void test() {
  int *s = NULL;
  if (s != NULL) {
    *s = 42;
  }
}

再次运行,不在汇报错误,问题修复。

在进行 C 文件分析时,Infer 使用 gcc 命令并在内部运行 clang 来解析。

因此,你可能会得到一些和 gcc 不一样的编译器警告以及错误。以下命令等效的:

infer -- gcc -c hello.c
infer -- clang -c hello.c

Hello world Android

为了能够分析 Android 应用,请确保已经安装最新的 Android SDK 22,并保持最新。当然还有Android SDK Build-tools 和 Android Support Repository

Android 的示例在 infer/examples/android_hello 。 编辑 local.properties 并设定本地 SDK 路径。

Android 示例使用 gradle 构建。不过你不需要下载和安装 gradle。 项目中的脚本 gradlew 会自动下载 gradle以及相关的项目依赖。

之后,运行:

infer -- ./gradlew build

Infer 会输出一系列问题:

MainActivity.java:20: error: NULL_DEREFERENCE
   object s last assigned on line 19 could be null and is dereferenced at line 20

MainActivity.java:37: error: RESOURCE_LEAK
   resource acquired by call to FileOutputStream(...) at line 34 is not released after line 37

增量分析

如果你没有改变任何文件,运行 Infer 进行检测,你会注意到,这次并没有进行分析。

这时因为 gradle 是增量的编译的。所有已经编译过的文件没有变动的文件将不会再编译。Infer 根据编译命令获取需要分析的文件,因此,这样的情况下,获取不到任何需要编译分析和分析的文件。

以下有两种解决方案:

  1. 在两次分析之间运行 gradlew clean

    ./gradlew clean
    

这将会使得每次都重新进行编译,所以 Infer 能获取到所有要分析的文件。

  1. 在增量模式下运行 Infer

    infer --incremental -- ./gradlew build
    

    这会使得 Infer 保留之前的编译结果。如果没有 --incremental (短命令是 -i ),Infer 会移除存储这些结果的文件夹: infer-out

你可以在 Infer workflow 页面中,详细了解这两种方案的细节。

Hello world iOS

iOS 的示例代码在这里 infer/examples/ios_hello,使用 Infer 检测:

infer -- xcodebuild -target HelloWorldApp -configuration Debug -sdk iphonesimulator

将会有以下问题输出:

AppDelegate.m:20: error: MEMORY_LEAK
   memory dynamically allocated to shadowPath by call to CGPathCreateWithRect() at line 20, column 28 is not reachable after line 20, column 5

AppDelegate.m:25: error: RESOURCE_LEAK
   resource acquired to fp by call to fopen() at line 25, column 8 is not released after line 25, column 5

AppDelegate.m:29: warning: PARAMETER_NOT_NULL_CHECKED
   Parameter callback is not checked for null, there could be a null pointer dereference: pointer callback could be null and is dereferenced at line 29, column 5

AppDelegate.m:34: error: NULL_DEREFERENCE
   pointer str last assigned on line 33 could be null and is dereferenced at line 34, column 12

AppDelegate.m:39: error: PREMATURE_NIL_TERMINATION_ARGUMENT
   pointer str last assigned on line 38 could be nil which results in a call to arrayWithObjects: with 1 arguments instead of 3 (nil indicates that the last argument of this variadic method has been reached) at line 39, column 12

Hello.m:20: error: NULL_DEREFERENCE
   pointer hello last assigned on line 19 could be null and is dereferenced at line 20, column 12

Hello.m:25: warning: IVAR_NOT_NULL_CHECKED
   Instance variable hello -> _hello is not checked for null, there could be a null pointer dereference: pointer ret_hello last assigned on line 24 could be null and is dereferenced at line 25, column 12

Hello.m:30: warning: PARAMETER_NOT_NULL_CHECKED
   Parameter hello is not checked for null, there could be a null pointer dereference: pointer ret_hello last assigned on line 29 could be null and is dereferenced at line 30, column 12

gradle 相似,需要使用 --incremental (或 -i) 使得增量编译有结果输出。

infer --incremental -- xcodebuild -target HelloWorldApp -configuration Debug -sdk iphonesimulator

或者在编译前清理:

xcodebuild -target HelloWorldApp -configuration Debug -sdk iphonesimulator clean

Hello world Make

使用 Infer 检测 C 代码,示例在 infer/examples/c_hello

infer -- make

将会输出以下问题:

example.c:22: error: NULL_DEREFERENCE
   pointer max last assigned on line 21 could be null and is dereferenced at line 22, column 10

example.c:36: error: NULL_DEREFERENCE
   pointer joe last assigned on line 35 could be null and is dereferenced by call to get_age() at line 36, column 10

example.c:45: error: RESOURCE_LEAK
   resource acquired to fd by call to open() at line 41, column 12 is not released after line 45, column 5

example.c:51: error: MEMORY_LEAK
   memory dynamically allocated to p by call to malloc() at line 51, column 14 is not reachable after line 51, column 3

example.c:57: error: MEMORY_LEAK
   memory dynamically allocated to p by call to malloc() at line 56, column 14 is not reachable after line 57, column 3

同样,和gradle 类似,为了使得增量编译下有结果输出,需要使用 --incremental (或 -i)

infer --incremental -- make

或者清理:

make clean

Infer 的工作机制

本页文档说明 Infer 的几种运行方式,你可根据你自己的项目具体情况选用。

摘要

  1. 初次运行时,确保项目是清理过的。可以通过 (make cleangradle clean 等等)
  2. 两次运行之间,记得清理项目,或者通过 --incremental 选项,方式因为增量编译而无结果输出。
  3. 如果你使用的是非增量编译系统,则无需如此,比如:infer -- javac Hello.java,编译 Java 文件。
  4. 成功运行之后,在同一目录下,你可以通过 inferTraceBugs 浏览更加详细的报告。

Infer 运行的两个阶段

不管是分析哪种语言,Infer 运行时,分为两个主要阶段:

1. 捕获阶段

Infer 捕获编译命令,将文件翻译成 Infer 内部的中间语言。

这种翻译和编译类似,Infer 从编译过程获取信息,并进行翻译。这就是我们调用 Infer 时带上一个编译命令的原因了,比如: infer -- clang -c file.cinfer -- javac File.java。结果就是文件照常编译,同时被 Infer 翻译成中间语言,留作第二阶段处理。特别注意的就是,如果没有文件被编译,那么也没有任何文件会被分析。

Infer 把中间文件存储在结果文件夹中,一般来说,这个文件夹会在运行 infer 的目录下创建,命名是 infer-out/。当然,你也可以通过 -o 选项来自定义文件夹名字:

infer -o /tmp/out -- javac Test.java

2. 分析阶段

在分析阶段,Infer 分析 infer-out/ 下的所有文件。分析时,会单独分析每个方法和函数。

在分析一个函数的时候,如果发现错误,将会停止分析,但这不影响其他函数的继续分析。

所以你在检查问题的时候,修复输出的错误之后,需要继续运行 Infer 进行检查,知道确认所有问题都已经修复。

错误除了会显示在标准输出之外,还会输出到文件 infer-out/bug.txt 中,我们过滤这些问题,仅显示最有可能存在的。

在结果文件夹中(infer-out),同时还有一个 csv 文件 report.csv,这里包含了所有 Infer 产生的信息,包括:错误,警告和信息。

增量模式和非增量模式

运行时,Infer 默认会删除之前产生的 infer-out/ 文件夹,这会导致非增量模式。

如果需要增量模式,加入 --incremental(或者 -i)参数运行,这样 infer-out/ 文件夹将不会被删除。

也有例外的情况,尤其是你只能在上面的一个阶段使用这个参数。

比如, infer -- javac Hello.java 相当于运行了以下两个命令。

infer -a capture -- javac Hello.java
infer -- analyze

注意,第二个命令不会删除 infer-out/,因为分析阶段,需要使用文件夹中的进行分析。

你可以通过 infer --help 了解更多关于 Infer 的各种操作模式。

下面我们简单明了地强调一下,什么情况下需要使用增量模式,什么时候使用非增量模式。

非增量模式

非增量模式适用于单编译命令,重复运行 Infer 检测的情况。

infer -- javac Hello.java
edit Hello.java
# 编译 Hello.java,改动了代码,比如修复了一些问题
infer -- javac Hello.java

如果需要进行全新的一轮的分析,必须:

  1. 删除结果文件夹:

    rm -fr infer-out
    
  2. 删除构建产物。比如,对于基于 make 的项目,运行 make clean

增量模式

许多软件项目都使用增量编译系统,比如手机应用。Infer 支持好些这样的编译系统,具体的看这个章节.

如果想使用 Infer 进行增量分析,你的编译系统需要是这其中的一个。

运行 Infer 进行检测的时候,只需要简单运行 infer -- <编译命令>,其中编译命令就是我们平时编译的命令。需要注意的是,运行前的项目是清理过的,这样 Infer 才能在捕获阶段捕获所有的编译命令。

比如,一个 gradle 项目:

gradle clean
infer -- gradle build

接下来,如果你修改了项目中的一些文件,你可以重复上面的命令,清理并重新分析整个项目。或者通过参数,让 Infer 使用使用增量模式。

edit some/File.java
# 修改了 some/File.java 的一些内容。
infer --incremental -- gradle build

当然,你也可以在第一次运行 Infer 进行检测的时候,就使用 --incremental

查看详细报告信息

在同一目录,通过命令 inferTraceBugs,你可以查看报告中的更多信息:

infer -- gradle build
inferTraceBugs

通过这个工具,你可以导致 bug 的错误堆栈,有助于追踪问题的详细原因。具体的,使用inferTraceBugs --help 查看使用帮助。

分析 APP 或者其他项目

使用 Infer 分析文件时,你可以使用 javac 或者 clang 编译器,当然你也可以使用 gcc。但在 Infer 内部会使用 clang 去编译你的代码。所以如果你的代码无法用 clang 编译的话,可能无法使用 Infer。

除此之外,你还可以和其他许多编译系统一起使用 Infer。注意一点,你可以通过并行编译命令来加快 Infer 的运行检测,比如:infer -- make -j8

如果你想分析整个项目的话,分析之前,记得清理项目。这样编译器才会重新编译所有文件,Infer 才会分析这些编译的文件(具体看 这个章节)。

以下是 Infer 目前支持的编译系统。对于某个特定的系统,你可以通过 infer --help -- <build system> 了解更多具体的信息。比如: infer --help -- gradle

Gradle

infer -- gradle <gradle task, e.g. "build">
infer -- ./gradlew <gradle task, e.g. "build">

Buck

infer -- buck <buck target>

Maven

infer -- mvn <maven target>

Xcodebuild

Infer 可是分析使用 xcodebuild 构建的应用,但是只分析 .m 和 .c 文件,其他的文件,比如:.cpp.cc.mm 文件会被忽略。

比如一个 iOS 应用:

infer -- xcodebuild -target <target name> -configuration <build configuration> -sdk iphonesimulator

Make

Infer 可以分析使用 make 构建的项目,项目中的 C++ 文件会被忽略。

infer -- make <make target>

Infer : Checkers

Infer 的分析器执行复杂的程序间(interprocedural,专注整体)静态分析。但当我们针对那些 linter 中常见的分析,不需要复杂的程序间的分析的时候,我们有一个称为 Infer:Checkers(Infer 校验器) 的框架。

Infer:Checkers 可以检测给定项目中每个方法的某个指定属性,虽然分析了整个项目,但是这种分析算是程序内的而不是程序间的。

通过选项 -a checkers 可以在分析时加入校验器(checkers),如下:

infer -a checkers -- javac Test.java

目前,我们有不可变转化校验器.

Infer : Eradicate

"I call it my billion-dollar mistake. It was the invention of the null reference in 1965."

Tony Hoare

什么是 Infer:Eradicate ?

Infer:Eradicate 是针对Java @Nullable 注解的一个检查器,是 Infer 静态分析工具套件中的一部分,目标是消除空指针异常。

@Nullable 注解指示一个参数,类成员,或者方法返回值可以是 null。

当这个注解修饰一个参数时,说明这个参数是允许为空的,方法体内部应该处理为空的情况。

当注解修饰一个参数时,说明方法的返回值是可以为空的。

从标注为 @Nullable 的程序开始,可空性将随着赋值和调用进行传播,分析器对这个流程敏感的传播过程进行分析。

分析之后,对那些未受保护的空值访问,前后不一致的@Nullable 注解或者该标记却没标记的方法或变量,加上错误标记。

Infer:Eradicate 也用来将之前未标记注解的代码添加注解。

什么是 @Nullable 约定?

通常对于一个对象,如果你什么都没说明,默认认为这个对象不会是空值。在可能的情况下,我们建议:

安全编程,注解空值。

如果可能为空值,即为类型参数加上 @Nullable 注解。

什么是注解

注解放在方法调用或者成员变量访问的接口中:

  • 定义方法时的参数和返回值类型
  • 成员变量申明

局部变量没有办法加注解,他们的可空性是推断出来的。

Infer:Eradicate 如何调用?

通过 -a eradicate 选项,可以启用 Eradicate,如下:

infer -a eradicate -- javac Test.java

对于这样的代码,试图访问一个可空的值,却没有做空检查,检测器会检查并报告错误。

class C {
  int getLength(@Nullable String s) {
    return s.length();
  }
}

但如果是以下这样,那么就没问题:

class C {
  int getLength(@Nullable String s) {
    if (s != null) {
      return s.length();
    } else {
      return -1;
    }
  }
}

Eradicate 会输出这些警告.

高级用法

如果你想详细了解 Infer 具体是如何工作的,或者想为 Infer 添砖加瓦,这个章节我们会具体讨论诸如调试选项,获取方法详细定义的办法等细节问题。

结果文件夹结构

检测成功运行之后,分析结果会被放入一个默认文件夹,infer-out

infer-out
├── captured/
├── log/
├── multicore/
├── sources/
├── specs/
├── bugs.txt
├── procs.csv
├── report.csv
├── report.json
└── stats.json
  • captured/ 包含了 Infer 分析需要的每个文件的信息,具体看 下面
  • log/multicore/, 和 sources/ 文件夹是分析器内部驱动所需。
  • specs/ 包含了所分析的各个方法的 参数指标,Infer 据此推断文件。
  • bugs.txtreport.csv, 和 report.json 为三种不同格式的分析结果。
  • procs.csv and stats.json 包含 debug 信息的分析结果。

捕获文件夹

每个被捕获进行分析的文件在 infer-out/captured 下都有一个对应的文件夹。比如有一个文件名为 example.c,那么便会有一个文件夹 infer-out/captured/example.c/

  • example.c.cfg
  • example.c.cg
  • example.c.tenv

.cfg, .cg 和 .tenv 后缀的文件包含了所分析文件的中间表示,这些数据传送给 Infer 的后端程序进行分析。 这些文件包含了序列化后的 OCaml 数据结构。.cfg 文件包含了代码文件中每个函数或方法的控制流程。.cg 包含了代码文件中定义的函数的调用关系,以及该文件对外部函数的调用关系。 .tenv 包含了代码文件中定义和用到的类型。

Debug 模式

通过 debug 选项 infer --debug -- <build command> 可输出调试信息。当使用 make 和clang 的时候,需要加一个额外的前端调试选项:

infer --frontend_debug --debug -- make example.c

在每个捕获文件夹中,会有一个 icfg.dot 文件,这个文件是 .cfg 和 call_graph.dot 文件的图表示,也就是说,这是调用关系的图表示。

另外,在 infer-out/captured 文件夹下,每个文件还有一个 html 页面文件。这个文件包含了源代码文件,文件的每一行,都有链接指向该行代码对应的控制流程的节点。点击各个节点的链接可以查看各个节点的对应的符号执行的详细情况。如果开启了 --no_test 选项,节点详细情况页面将会包含打印出来的所有符号执行的情况。

输出方法的详细定义

使用 InferPrint 可以输出 Infer 创建的方法的详细定义,你可以输出一个方法或者所有方法的详细定义,如下:

class Hello {
    int x;
    void setX(int newX) {
        this.x = newX;
    }
}

运行:

    infer -- javac Hello.java

setX 的详细定义储存在 infer-out/specs,我们可以如下打印输出:

    InferPrint infer-out/specs/Hello.setX{98B5}:void.specs

Java 方法的命名规则为,<class name>.<method name>,输出如下:

Procedure: void Hello.setX(int)
void void Hello.setX(int)(class Hello *this, int newX)
Timestamp: 1
Status: INACTIVE
Phase: RE_EXECUTION
Dependency_map:
TIME:0.006893 s TIMEOUT:N SYMOPS:34 CALLS:0,0
ERRORS:
--------------------------- 1 of 1 [nvisited: 4 5 6] ---------------------------
PRE:
this = val$1: ;
newX = val$3: ;
this|->{Hello.x:val$2}:
POST 1 of 1:
this = val$1: ;
return = val$4: ;
newX = val$3: ;
this|->{Hello.x:newX}:
----------------------------------------------------------------

以上可见,方法执行最开始,this 需要初始化,在方法最后,x 和 newX 是相等的。

如果要打印结果文件夹下所有的方法的详细定义:

InferPrint -results_dir infer-out

运行内部测试用例

在 Infer 的代码中,有很多测试用例。我们使用这些测试用例来检验 Infer 行为的正确性。

运行这些测试用例时候,我们使用 Facebook 的另外一个开源项目 Buck 来构建项目,我们有一个inferTest 脚本来运行这些测试用例。使用这个脚本需要确保 buck 路径在 PATH 变量中。

inferTest java    # Run the tests about Java analysis
inferTest clang   # Run the tests about C and Objective-C analysis
inferTest c       # Run the tests about C analysis
inferTest objc    # Run the tests about Objective-C analysis

模块

为什么需要模块

当我们分析一个项目时, 函数之间相互依赖。Infer 会跟随调用关系决定以什么样的一个顺序分析这些函数。这样做的目的是不管在哪里调用该方法时,总能用上这个方法的分析概要。

我们举个例子来说明:

int foo(int x) {
  if (x < 42) {
    return x;
  } else {
    return 0;
  }
}

int bar() {
  return foo(24);
}

int baz() {
  return foo(54);
}

从 foo 开始,Infer 发现这个函数要么在参数小于 42 时返回 0,要么直接返回参数的值。根据这个信息,Infer 便可以发现,bar 总是返回 24 而 baz 总是返回 0

当然,在分析的过程中,一些函数的代码可能不存在。比如,一个使用预编译库的项目,最典型的情况就是我们是一共标准库的时候,如下:

#include <stdlib.h>

int* create() {
  int *p = malloc(sizeof(int));
  if (p == NULL) exit(1);
  return p;
}

void assign(int x, int *p) {
  *p = x;
}

int* my_function() {
  int *p = create();
  assign(42, p);
  return p;
}

在上面的例子中,当 Infer 首先会分析 create 函数,当这时 malloc 对应的源码是找不到的。为了处理这种情况,Infer 依赖这些没有源码的函数的模块才能进行分析。malloc 函数在内部模块化为要么返回 NULL 要么返回一个有效的分配后的内存指针。类似地,exit 函数被模块化为退出执行。使用这两个模型,Infer 发现 create 总是发现一个分配后的指针,my_function 是安全的。

这时,我们必须要注意,缺失源码和模块都不会导致分析失败。源码缺失的函数被当做无效函数。虽然在大多数情况下,会直接跳过这些函数不会有问题,但这有可能影响到分析的质量。比如缺失的模块将有可能导致生成不正确的 bug 报告。

我们现在来考虑这样一种情况:有一个函数 lib_exit 和 exit 有相同的作用,不过这个函数被定义在一个不属于这个项目的预编译库中。

void lib_exit(int);

int* create() {
  int *p = malloc(sizeof(int));
  if (p == NULL) lib_exit(1);
  return p;
}

这时,Infer 只有在 p 非空的时候,才有可能知道返回值。当分析 my_function 时,Infer 会考虑参数为空的情况,并在调用 assign(42, p) 处报一个空引用错误。

相同地, 我们有 lib_alloc 和 malloccreate 定义如下:

int* lib_alloc(int);

int* create() {
  int *p = lib_alloc(sizeof(int));
  return p;
}

这时,在 my_function 却不会报任何空引用错误。因为 lib_alloc 源码缺失,返回总是 NULL

示例

C

添加新的模块很简单,C 的模块在 infer/models/c/src/. The file libc_basic.c。这包含了为 C 标准库中最常用的函数定义的模块。 比如,xmalloc ,本质上和上述 create 是一样的。模块定义如下:

void *xmalloc(size_t size) {
  void *ret = malloc(size);
  INFER_EXCLUDE_CONDITION(ret == NULL);
  return ret;
}

The function xmalloc is modeled using malloc to create an allocated object and the macroINFER_EXCLUDE_CONDITION used to eliminate the case where malloc can return null. The list of helper functions and macros for writing models can be found in infer_builtins.c.

For a slightly more complex example, realloc is modeled as:

void *realloc(void *ptr, size_t size) {
  if(ptr==0) { // if ptr in NULL, behave as malloc
    return malloc(size);
  }
  int old_size;
  int can_enlarge;
  old_size = __get_array_size(ptr); // force ptr to be an array
  can_enlarge = __infer_nondet_int(); // nondeterministically choose whether the current block can be enlarged
  if(can_enlarge) {
    __set_array_size(ptr, size); // enlarge the block
    return ptr;
  }
  int *newblock = malloc(size);
  if(newblock) {
    free(ptr);
    return newblock;
  }
  else { // if new allocation fails, do not free the old block
    return newblock;
  }
}

This model is based on existing models for malloc and free and three helper functions:

  • __get_array_size(ptr) which allows to manipulate with a model what Infer knows about the size of the allocated memory
  • __set_array_size(ptr, size) to modify the information about the size of the allocated memory
  • __infer_nondet_int() to create a variable which can have any possible integer value

For Java

The models for Java are following the same approach and the list of helper functions is in:

infer/models/java/src/com/facebook/infer/models/InferBuiltins.javainfer/models/java/src/com/facebook/infer/models/InferUndefined.java

For example, Infer treats Java hash maps using a recency abstraction model: Infer remembers the last two keys being added by put and checked by containsKey, which can be used to make sure that no null pointer exceptions are coming from the fact that get(key) returns null when keyis not not in the map. This behavior can just be implemented via a model written in Java with the help of few helper functions understood by Infer. These models can be found in:

infer/models/java/src/java/util/HashMap.java

and just rely on these two methods:

  • InferUndefined.boolean_undefined() to create a non-deterministic choice
  • (V)InferUndefined.object_undefined() to create a non null undefined object of type V

How to add new models

Let's look at a toy example in Java. As explained above, models for C, Objective-C and Java are all following the same approach.

import lib.Server;

public class Test {

  enum Status {
    SUCCESS, FAILURE, PING_FAILURE, CONNECTION_FAILURE
  }

  Status convertStatus(Server s) {
    switch (s.getStatus()) {
    case 0:
      return Status.SUCCESS;
    case 1:
      return Status.FAILURE;
    case 2:
      return Status.FAILURE;
    default: // should not happen
      return null;
    }
  }

  String statusName(Server s) {
    Status status = convertStatus(s);
    return status.name();
  }

}

Assuming that the class lib.Server is part of a pre-compiled library, Infer will report a null pointer exception in statusName. This happens whenever s.getStatus() returns a value greater that3, in which case the default branch of the switch statement is taken and convertStatus returnsnull. However, we know from the documentation that the method lib.Server.getStatus can only return 01, or 2. A possible approach would be to use an assertion like the GuavaPreconditions.checkState to inform Infer about the invariant:

Status convertStatus(Server s) {
  int serverStatus = s.getStatus();
  Preconditions.checkState(serverStatus >= 0 && serverStatus < 3);
  switch (s.getStatus()) {
    ...
  }
}

However, in the case where adding preconditions is not possible, we can then write a model forgetStatus() in order to make the analysis more precise.

To create a model for getStatus(), we need to add a class with the name and the same package as for the original method. In this example:

  • create a file infer/models/java/src/infer/models/Server.java with the following content:

    package infer.models;
    
    import com.facebook.infer.models.InferBuiltins;
    import com.facebook.infer.models.InferUndefined;
    
    public class Server {
    
      public int getStatus() {
        int status = InferUndefined.int_undefined();
        InferBuiltins.assume(status >= 0 && status < 3);
        return status;
      }
    }
    
  • recompile infer:

    make -C infer
    
  • run the analysis again:

    infer -- javac Test.java
    

Now it should no longer report a null pointer exception.

Infer bug types

Here is an overview of the types of bugs currently reported by Infer.

Resource leak

Infer reports resource leaks in C, Objective-C and Java. In general, resources are entities such as files, sockets, connections, etc, that need to be closed after being used.

Resource leak in C

This is an example of a resource leak in C code:

-(void) resource_leak_bug {
    FILE *fp;
    fp=fopen("c:\\test.txt", "r"); // file opened and not closed.
}

Resource leak in Java

For the remaining of this section, we will consider examples of resource leaks in Java code.

TIP: A common source of bugs is exceptions skipping past close() statements. That is the first thing to look for if INFER reports a potential resource leak.

Basics and Standard Idiom

Some objects in Java, the resources, are supposed to be closed when you stop using them, and failure to close is a resource leak. Resources include input streams, output streams, readers, writers, sockets, http connections, cursors, and json parsers.

The standard idiom is

  // Standard idiom
  Allocate resource
  try {
    do some stuff
  } finally {
    close resource
  }

or more for example,

  //  Standard Idiom
  public static void foo () throws IOException{
    FileOutputStream fos = new FileOutputStream(new File("whatever.txt"));
    try {
      fos.write(7);
    } finally {
      fos.close();
    }
  }

and you should use the standard idiom for the most part, when you don't want to return the resource to the surrounding context.

Sometimes people just leave out close(), and that is a bug, but more typically exceptional paths are the root of the problem, as in

  // leak because of exception
  public static void foo () throws IOException {
    FileOutputStream fos = new FileOutputStream(new File("whatever.txt"));
    fos.write(7);   //DOH! What if exception?
    fos.close();
  }

where an exception in fos.write will cause execution to skip past the close() statement.

Multiple Resources Bugs

We can deal with multiple resources correctly and simply just by nesting the standard idiom.

  // Two Resources nested
  public static void foo() throws IOException {
    FileInputStream fis = new FileInputStream(new File("whatever.txt"));
    try {
      FileOutputStream fos = new FileOutputStream(new File("everwhat.txt"));
      try {
        fos.write(fis.read());
      } finally {
        fos.close();
      }
    } finally {
      fis.close();
    }
  }

Bugs often occur when using multiple resources in other ways because of exceptions in close() methods. For example,

  // Classic Two Resources Bug
  public static void foo() throws IOException {
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
      fis = new FileInputStream(new File("whatever.txt"));
      fos = new FileOutputStream(new File("everwhat.txt"));
      fos.write(fis.read());
    } finally {
      if (fis!=null)  fis.close();
      if (fos!=null) fos.close();
    }
  }

Here, if there is an exception in the call to fis.close() execution will skip past fos.close(); a leak.

Another way, besides the standard idiom, to deal with this problem is to swallow exceptions.

  // Two Resources Fix 1
  public static void foo() throws IOException {
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
      fis = new FileInputStream(new File("whatever.txt"));
      fos = new FileOutputStream(new File("everwhat.txt"));
      fos.write(fis.read());
    } finally {
      try {
        if (fis!=null) fis.close();
      } catch (Exception e) {};  // Exception swallowing
      if (fos!=null) fos.close();
    }
  }

You can also swallow the exception on the output stream. Some people prefer not to swallow output stream exceptions, and also flush before closing. http://code.google.com/p/guava-libraries/issues/detail?id=1118

Notice that the nested standard idiom does not need the checks for null, which are in there in this case to protect against the case when one of the allocations throws an exception, in which case one would get a NullPointerException.

Nested_Allocations

When a resource allocation is included as an argument to a constructor, if the constructor fails it can leave an an unreachable resource that no one can close.

For example gzipOutputStream = new GZIPOutputStream(new FileOutputStream(out)); is bad in case the outer constructor, GZIPOutputStream, throws an exception. In that case, no one will have a hold of the FileOutputStream and so no one will be able to close it.

In such a case you need to move the allocation the FileOutputStream out of the nested position and name it, so you are able to close if anything goes wrong during execution of the GZIPOutputStream constructor.

Here are resources that can throw exceptions i their constructor(s).

  • ObjectInputStream , ObjectOutputStream, PipedInputStream, PipedOutputStream, PipedReader, PipedWriter, JarInputStream, JarOutputStream, GZIPInputStream, GZIPOutputStream , ZipFile all throw IOException
  • PrintStream throws UnsupportedEncodingException

The constructors for FileInputStream, FileOutputStream and RandomAccessFile throw FileNotFoundException, but these cases are not problematic in the sense that their arguments are not resources and so they do not cause the nested resource leak.

Allocation of JSonParser and Cursor resources

Some resources are created inside libraries instead of by "new".

Cursor is an interface, the actual resources are something like SQLiteCursor. So, every time you call a function that returns a Cursor object, there is an allocation.

For instance, in the functions from SQLiteDatabase query(…) and rawQuery(…) allocate a cursor resource. For SQLiteQueryBuilder, ContentProviderClient, ContentResolver. MediaStore and DownloadManager it is only query(…) Cursor objects cursor created by these functions need to be closed (i.e., cursor.close()).

Similarly, JsonParser is an abstract class, and create a resource in functions from the class JsonFactory createParser(byte[] data) createParser(byte[] data, int offset, int len) createParser(String content) createParser(URL url) createParser(File f) JsonParser objects js created by these functions need to be closed (jp.close()). On the other hand . JasonParsers gotten from createParser(InputStream in) and createParser(Reader r) give you JsonParsers that don’t need to be closed. This is because they receive the resource from somewhere that will maintain the responsibility to close it.

Escaping resources and exceptions

Sometimes you want to return a resource to the outside, in which case you should not close it, but you still need to be careful of exceptions in case control skips past the return leaving no one to close. Here is a simple example of a positive use of escaping resources.

  // An escaping resource, shouldn't close
  public BugReportAttachment createAttachment(File reportDirectory, String fileName)
      throws FileNotFoundException {
    File file = new File(reportDirectory, fileName);
    OutputStream stream = new FileOutputStream(file);
    return new BugReportAttachment(Uri.fromFile(file), stream);
  }

In this case it is intended that an object that wraps stream is passed to the caller ofcreateAttachment. You should certainly not close stream here, because it is being passed to the outside.

But for escaping resources like this you still need to be careful of exceptions. For example, in

  // An escaping resource, and a leak
  public BugReportAttachment createAttachment(File reportDirectory, String fileName)
      throws FileNotFoundException {
    File file = new File(reportDirectory, fileName);
    OutputStream stream = new FileOutputStream(file);
    stream.write(7);
    return new BugReportAttachment(Uri.fromFile(file), stream);
  }

if stream.write(7) throws an exception, then no one will have a hold of stream, and no one will be able to close it; a leak.

Java 7's try-with-resources

(For use only if you have are using Java 7)

Clearly, accounting for the ramifications of all the exceptional cases is complicated, and there is a better way in Java 7.

  // Two Resources Fix 2; via try-with-resources
  public static void foo() throws IOException {
    try (
      FileInputStream fis = new FileInputStream(new File("whatever.txt"));
      FileOutputStream fos = new FileOutputStream(new File("everwhat.txt"))
    ) {
      fos.write(fis.read());
    }
  }

All the complicated exceptional cases above are (apparently) covered by this construct, and the result is much simpler.

So, if you are trying to fix a potential leak in code with multiples resources you can go ahead and try to understand whether the potential leak is real. Or, if the code is complex and it is hard to figure out, it would be perfectly legitimate to simply convert the code over to try-with-resources if you have access to Java 7, so as to save yourself some brain-cycles. You will also end up with cleaner code.

If try-with-resources is so great you should always use it. But you shouldn't… Try-with-resources gives resources static scoping, and works via a stack discipline. Sometimes, you want a resource to persist beyond scope, as in the escaping example above. In an escaping example maybe you could refactor lots of code so that try-with-resources applies, and maybe you cannot in a sensible way. This just illustrates that, though you might hear people say that try-with-resources "solves" the resource problem, it does not. It is very useful, but you cannot use it blindly when you see a resource-allocation site.

Memory leak

Memory leak in C

This error type is only reported in C and Objective-C code. In Java we do not report memory leaks because it is a garbage collected language.

In C, Infer reports memory leaks when objects are created with malloc and not freed. For example:

-(void) memory_leak_bug { 
    struct Person *p = malloc(sizeof(struct Person));
}

Memory leak in Objective-C

Additionally, in Objective-C, Infer reports memory leaks that happen when objects from Core Foundation or Core Graphics don't get released.

-(void) memory_leak_bug_cf { 
    CGPathRef shadowPath = CGPathCreateWithRect(self.inputView.bounds, NULL); //object created and not released.
}

Retain cycle

A retain cycle is a situation when object A retains object B, and object B retains object A at the same time. Here is an example:

@class Child;
@interface Parent : NSObject {
    Child *child; // Instance variables are implicitly __strong
}
@end
@interface Child : NSObject {
    Parent *parent;
}
@end

You can fix a retain cycle in ARC by using __weak variables or weak properties for your "back links", i.e. links to direct or indirect parents in an object hierarchy:

@class Child;
@interface Parent : NSObject {
    Child *child;
}
@end
@interface Child : NSObject {
    __weak Parent *parent;
}
@end

Null Dereference

Infer reports null dereference bugs in C, Objective-C and Java. The issue is about a pointer that can be null and it is dereferenced. This leads to a crash in all the above languages.

Null dereference in C

Here is an example of an inter-procedural null dereference bug in C:

struct Person {
  int age;
  int height;
  int weight;
};
int get_age(struct Person *who) {
  return who->age;
}
int null_pointer_interproc() {
  struct Person *joe = 0;
  return get_age(joe); 
}

Null dereference in Objective-C

In Objective-C, null dereferences are less common than in Java, but they still happen and their cause can be hidden. In general, passing a message to nil does not cause a crash and returnsnil, but dereferencing a pointer directly does cause a crash as well as calling a nil block.

-(void) foo:(void (^)())callback {
    callback();
}

-(void) bar {
    [self foo:nil]; //crash
}

Moreover, there are functions from the libraries that do not allow nil to be passed as argument. Here are some examples:

-(void) foo {
    NSString *str = nil;
    NSArray *animals = @[@"horse", str, @"dolphin"]; //crash
}

-(void) bar {
  CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); //can return NULL
  ...
  CFRelease(colorSpace); //crashes if called with NULL
}

Null dereference in Java

Many of Infer's reports of potential NPE's come from code of the form

  p = foo(); // foo() might return null
  stuff();
  p.goo();   // dereferencing p, potential NPE

If you see code of this form, then you have several options.

If you are unsure whether or not foo() will return null , you should ideally i. Change the code to ensure that foo() can not return null ii. Add a check for whether p is null, and do something other than dereferencing p when it is null.

Sometimes, in case ii it is not obvious what you should do when p is null. One possibility (a last option) is to throw an exception, failing early. This can be done using checkNotNull as in the following code:

  // code idiom for failing early

  import static com.google.common.base.Preconditions.checkNotNull;

  //... intervening code

  p = checkNotNull(foo()); // foo() might return null
  stuff();
  p.goo();   // dereferencing p, potential NPE

The call checkNotNull(foo()) will never return null; in case foo() returns null it fails early by throwing an NPE.

If you are absolutely sure that foo() will not be null , then if you land your diff this case will no longer be reported after your diff makes it to master. In the future we might include analysis directives (hey, analyzer, p is not null!) like in Hack that tell the analyzer the information that you know, but that is for later.

Parameter not null checked

This error type is reported only in Objective-C. It is similar to Null dereference, but Infer hasn't found a whole trace where the error can happen, but only found that a null dereference can happen if you call a method with nil as an argument. Therefore it is only a warning. For example:

  -(int) foo:(A* a) {
      B b* = [a foo]; // sending a message with receiver nil returns nil
      return b->x; // dereferencing b, potential NPE if you pass nil as the argument a.
  }

or when the parameter is a block:

   -(void) foo:(void (^)(BOOL))block {
      block(YES); // calling a nil block will cause a crash.
   }

Possible solutions are adding a check for nil, or making sure that the method is not called withnil.

Ivar not null checked

This error type is only reported in Objective-C. This is similar to Null dereference, but Infer hasn't found a whole trace where the error can happen, but only found that a null dereference can happen if an instance variable of a parameter is nil. For example:

  -(int) foo {
      B b* = [self->_a foo]; // sending a message with receiver nil returns nil
      return b->x; // dereferencing b, potential NPE if you pass nil as the argument a.
  }

Possible solutions are adding a check for nil, or making sure that the method is not called withnil.

Premature nil termination argument

This error type is reported in C and Objective-C. In many variadic methods, nil is used to signify the end of the list of input objects. This is similar to nil-termination of C strings. If one of the arguments that is not the last argument to the method is nil as well, Infer reports an error because that may lead to unexpected behavior.

An example of such variadic methods is arrayWithObjects

  NSArray *foo = [NSArray arrayWithObjects: @"aaa", str, @"bbb", nil];

In this example, if str is nil then an array @[@"aaa"] of size 1 will be created, and not an array@[@"aaa", str, @"bbb"] of size 3 as expected.

校验器报告的问题类型

不可变转化校验器

这个问题会在 Java 代码中出现。当函数返回类型是一个可变集合,但是实际返回的是不可变集合。

  public List<String> getSomeList() {
    ImmutableList<String> l = foo(...);
    return l;
  }

如果对 getSomeList 的返回值进行操作,比如添加一些数据项,将会触发运行时错误。

至于修改方案,你可以修改返回类型,也可以把集合数据拷贝一份,变成可变集合。


Eradicate warnings

Below you will find a description of all the warnings reported by Eradicate.

Eradicate null field access

A field access of the form x.field where x could be null.

Example:

class C {
  void foo(@Nullable C x) {
    x.field = 3;
  }
}

Action: Make sure that x cannot be null by changing the code or changing annotations. If this cannot be done, the only choice is to use defensive programming: if (x != null) { ... x.field ... } else { ... you need to decide what to do when x is null ... } The general recommendation is to push null checks up the call chain as much as possible in order to detect the place where null values originate and deal with them at that point. When a null value is propagated down the call chain it is often difficult to determine its origin without global knowledge of what the program does. For example, a null value could originate in third party libraries which are not under your control, and the best place to check for null is typically immediately after calling these library functions.

Eradicate null method call

A method call x.m(...) where x could be null.

Example:

class C {
  void foo(@Nullable C x) {
    String s = x.toString();
  }
}

Action: Same as for Null field access.

Eradicate field not nullable

An assignment x.f = v where v could be null and field f is not annotated with @Nullable.

Example:

class C {
  String f;

  void foo(@Nullable String s) {
    f = s;
  }
}

Action: The preferred action is to ensure that a null value is never stored in the field, by changing the code or changing annotations. If this cannot be done, add a @Nullable annotation to the field. This annotation might trigger more warnings in other code that uses the field, as that code must now deal with null values.

Eradicate field not initialized

The constructor does not initialize a field f which is not annotated with @Nullable

Example:

class C {
  String f;

  C () { // field f not initialized and not annotated @Nullable
  }
}

Action: The preferred action is to initialize the field with a value that is not null. If, by design, null is a valid value for the field, then it should be annotated with @Nullable.

Eradicate parameter not nullable

Method call x.m(..., v, ...) where v can be null and the corresponding parameter in method m is not annotated with @Nullable

Example:

class C {
  void m(C x) {
    String s = x.toString()
  }

  void test(@Nullable C x) {
    m(x);
  }
}

Action: The preferred action is to ensure that a null value is never passed to the method, by changing the code or changing annotations. If this cannot be done, add a @Nullable annotation to the relevant parameter in the method declaration. This annotation might trigger more warnings in the implementation of method m, as that code must now deal with null values.

Eradicate return not nullable

Method m can return null, but the method's return type is not annotated with @Nullable

Example:

class C {
  String m() {
    return null;
  }
}

Action: The preferred action is to ensure that a null value is never returned by the method, by changing the code or changing annotations. If this cannot be done, add a @Nullable annotation to the the method declaration. This annotation might trigger more warnings in the callers of method m, as the callers must now deal with null values.

Eradicate condition redundant

This report is inactive by default. Condition (x != null) or (x == null) when x cannot be null: the first condition is always true and the second is always false

Example:

class C {
  void m() {
    String s = new String("abc");
    if (s != null) {
      int n = s.length();
    }
  }
}

Action: Make sure that the annotations are correct, as the condition is considered redundant based on the existing annotations. In particular, check the annotation of any input parameters and fields of the current method, as well as the annotations of any method called directly by the current method, if relevant. If the annotations are correct, you can remove the redundant case.

Eradicate return overannotated

This report is inactive by default. Method m is annotated with @Nullable but the method cannot return null

Example:

class C {
  @Nullable String m() {
    String s = new String("abc");
    return s;
  }
}

Action: Make sure that the annotations are correct, as the return annotation is considered redundant based on the existing annotations. In particular, check the annotation of any input parameters and fields of the current method, as well as the annotations of any method called directly by the current method, if relevant. If the annotations are correct, you can remove the @Nullable annotation.

Eradicate inconsistent subclass return annotation

The return type of the overridden method is annotated @Nullable, but the corresponding method in the superclass is not.

Action: choose a consistent annotation based on the desired invariant.

Example:

class A {
  String create() {
    return new String("abc");
  }
}

class B extends A {
  @Nullable String create() {  // Inconsistent @Nullable annotation.
      return null;
  }
}

A consistent use of @Nullable on the return type across subtyping should prevent runtime issue like in:

class Main {

  int foo(A a) {
     String s = a.create();
     return s.length();
  }

  void main(String[] args) {
     A a = new B();
     foo(a);
  }

}

Inconsistent subclass parameter annotation

A parameter of the overridden method is missing a @Nullable annotation present in the superclass.

Action: choose a consistent annotation based on the desired invariant.

Example:

class A {

  int len(@Nullable String s) {
    if (s != null) {
      return s.length();
    } else {
      return 0;
    }
  }
}

class B extends A {

  int len(String s) {  // @Nullable missing.
    return s.length();
  }
}

A consistent use of @Nullable on parameters across subtyping should prevent runtime issue like in:

public class Main {

  String s;

  int foo() {
    A a = new B();
    return a.len(s);
  }
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值