CodeQL学习笔记
CodeQL学习笔记,大部分翻译自官方文档
安装
下载CodeQL CLI
https://github.com/github/codeql-cli-binaries/releases
解压至/xxx/CodeQL目录并添加环境变量export PATH=/xxx/CodeQL/codeql:$PATH
下载包含标准库的工作空间
1 | cd /xxx/CodeQL/ |
安装VSCode CodeQL扩展
VSCode商店搜索安装CodeQL,并在扩展设置中设置CodeQL引擎路径/xxx/CodeQL/codeql/codeql
测试
在VSCode 中,单击文件 > 打开工作区,选择vscode-codeql-starter目录下的vscode-codeql-starter.code-workspace文件
在VSCode左侧选择CodeQL插件页面,选择一个CodeQL数据库,然后在文件vscode-codeql-starter/codeql-custom-queries-java/example.ql单击右键选择CodeQL:Run Query
基础命令
生成数据库
1 | codeql database create /xxx/CodeQL/databases/Test --language="java" --source-root=/xxx/Demo --command="mvn clean package -Dmaven.test.skip=true" |
/xxx/CodeQL/databases/Test 指定生成的数据库位置
–source-root 项目源码路径
–command 编译命令,PHP和Python等不需要。对于Maven,Ant等项目也可以省略
如果没有指定--command,CodeQL会根据平台的不同,调用./java/tools/autobuild.cmd或./java/tools/autobuild.sh对项目进行分析。如果该项目的编译工具为Gradle、Maven或Ant,且能找到相应的配置文件。程序就会进入相应的流程,调用相关的编译指令对项目进行编译。CodeQL会收集项目编译过程中产生的信息,并以此生成数据库。如果不属于Gradle、Maven、Ant中任意一种,则报错退出。
CodeQL元数据
CodeQL查询的元数据作为 QLDoc 注释的内容包含在每个查询文件的顶部。此元数据告诉 LGTM 和VSCode 的 CodeQL 插件如何处理查询并正确显示其结果。
例:
1 | /** |
1 | /** |
一些示例
Java污点跟踪_GetenvSource-URLSink
1 | /** |
Python污点跟踪_RemoteFlowSource-FileSystemAccessSink
1 | /** |
CodeQL for Java
Java代码的基础查询
以下查询为查找多余的if语句,即then分支是空的,如if (...) { }
1 | import java |
import java
导入适用于 Java 的标准 CodeQL 库,每个查询都以一个或多个import语句开始
from IfStmt ifstmt, BlockStmt blockstmt
定义查询的变量,声明的形式为:<type> <variable name>
IfStmt:if语句
BlockStmt:语句块
where ifstmt.getThen() = blockstmt and blockstmt.getNumStmt() = 0
定义变量的条件,ifstmt.getThen() = blockstmt将这两个变量联系起来。blockstmt必须是if语句的then分支。blockstmt.getNumStmt() = 0声明该块必须为空(即不包含任何语句)
IfStmt::getThen:Stmt getThen(),成员谓词,获取此if语句的then分支
BlockStmt::getNumStmt:int getNumStmt(),成员谓词,获取此块中直接子语句的数目
select ifstmt, “This ‘if’ statement is redundant.”
定义每个匹配项的报告内容,select用于查找不良编码实例的查询语句始终采用以下形式:select <program element>, "<alert message>"
浏览查询结果可能会发现带有else分支的if语句的例子,其中空的then分支确实起到了作用。例如:
1 | if (...) { |
在这种情况下,将带有空then分支的if语句识别为多余的是错误的。一种解决方案是如果if语句有else分支,则忽略空的then分支:
1 | import java |
IfStmt::getElse:
Stmt getElse(),成员谓词,获取此if语句的else分支
CodeQL的Java库
标准 Java 库中最重要的类可以分为以下五个类别
- 表示程序元素的类(例如Java的类和方法)
- 表示 AST 节点的类(例如语句和表达式)
- 表示元数据的类(例如注释和注解)
- 计算度量的类(例如圈复杂度和耦合度)
- 导航程序调用图的类
程序元素
包括包(Package)、编译单元(CompilationUnit)、类型(Type)、方法(Method)、构造函数(Constructor)和变量(Variable)
它们的共同超类是Element,它提供了通用的成员谓词,用于确定程序元素的名称和检查两个元素是否相互嵌套
Callable是 Method 和Constructor的共同超类,通过Callable引用一个可能是方法或构造函数的元素通常很方便
类型
Type类有许多子类用于表示不同类型:
PrimitiveType表示一个 基本类型,即boolean,byte,char,double,float,int,long,short之一, QL 也将void和<nulltype>归为基本类型RefType表示引用类型,包含如下子类:ClassJava类InterfaceJava接口EnumTypeJava枚举类型ArrayJava数组类型
例如,以下查询查找程序中为int类型的所有变量:
1 | import java |
Variable::getType:
Type getType(),获取变量的类型
Element::hasName:predicate hasName(string name),如果元素具有指定的名称则该谓词成立
引用类型也根据其声明范围进行分类:
TopLevelType表示在编译单元的顶层声明的引用类型NestedType是在另一个类型中声明的类型
例如,此查询查找名称与其编译单元名称不同的所有顶级类型:
1 | import java |
Element::getName:
string getName(),获取元素的名称
RefType::getCompilationUnit:CompilationUnit getCompilationUnit(),获取声明此类型的编译单元
CompilationUnit::getName:string getName(),获取编译单元的名称(不包括其扩展名)
还有几个专用的类:
TopLevelClass表示在编译单元的顶层声明的类NestedClass表示在另一个类型内声明的类,如LocalClass, 是在方法或构造函数中声明的类.AnonymousClass, 匿名类
最后,该库还有许多封装了常用的Java标准库类的单例类:
TypeObject、TypeCloneable、TypeRuntime、TypeSerializable、TypeString、TypeSystem和TypeClass
例如,我们可以编写一个查询,查找直接继承Object的所有嵌套类:
1 | import java |
RefType::getASupertype:
RefType getASupertype(),获取此类型的直接超类
泛型
Type还有几个子类用于处理泛型类型GenericType代表 GenericInterface 或 GenericClass,它表示一个泛型类型声明,比如java.util.Map接口
1 | package java.util.; |
类型参数,如本例中的K和V,由类TypeVariable表示
泛型类型的参数化实例提供了一个具体类型来实例化类型参数,如 Map<String, File>中所示。这样的类型由 ParameterizedType表示,它不同于表示其实例化来源的泛型类型GenericType。要从ParameteredType转换为相应的GenericType,可以使用谓词getSourceDeclaration。例如,我们可以使用下面的查询来查找所有java.util.Map的参数化实例:
1 | import java |
RefType::hasQualifiedName:
predicate hasQualifiedName(string package, string type),如果在指定名称的指定包中声明了此类型,则该谓词成立。对于嵌套类型来说,名称以$作为前缀,并附加到封闭类型的名称之后,封闭类型也可能是嵌套类型
RefType::getSourceDeclaration:RefType getSourceDeclaration(),获取此类型的源声明。对于泛型类型和原始类型的参数化实例,源声明是相应的泛型类型。对于泛型类型的参数化实例中声明的非参数化类型,源声明是泛型类型中的相应类型。对于其他所有类型,源声明就是类型本身
一般来说,泛型类型可能会限制类型参数可以绑定到的类型。例如,一种从字符串到数字的映射可以声明如下:
1 | class StringToNumMap<N extends Number> implements Map<String, N> { |
这意味StringToNumberMap的参数化实例只能用Number或其子类来实例化类型参数N。我们说N是一个有界类型参数,其上界是Number。在QL中,可以使用谓词GetAtypeBind查询类型变量的类型绑定。类型约束本身由类TypeBound表示,该类有一个成员谓词getType来检索变量被绑定的类型。例如,以下查询将查找所有类型绑定为Number的类型变量:
1 | import java |
BoundedType::getATypeBound:
TypeBound getATypeBound(),获取此类型的绑定类型(如果有的话)
为了处理不知道泛型的遗留代码,每个泛型类型都有一个没有任何类型参数的“原始”版本。在CodeQL库中,原始类型使用类RawType表示,该类有预期的子类RawClass和RawInterface。同样,还有一个用于获取相应泛型类型的谓词getSourceDeclaration。例如,我们可以查询(原始)Map类型的变量:
1 | import java |
在以下代码段中,此查询将找到m1,而不是m2:
1 | Map m1 = new HashMap(); |
最后,变量可以声明为通配符类型:
1 | Map<? extends Number, ? super Float> m; |
通配符 ? extends Number 和 ? super Float 由类WildcardTypeAccess表示。与类型参数一样,通配符可能有类型界限。与类型参数不同的是通配符可以有上界(如 ? extends Number)和下界(如 ? super Float)。类WildcardTypeAccess提供了成员谓词getUpperBound和getLowerBound,分别用于检索上界和下界
为了处理泛型方法,有类GenericMethod、ParameterizedMethod和RawMethod,它们完全类似于用于表示泛型类型的类似命名类。
有关Java类型的更多信息,见Java中的类型
变量
类Variable表示Java意义上的变量,它要么是类的成员字段(无论是静态的还是非静态的),要么是局部变量,要么是参数。所以针对这些特殊情况,有三个子类:
Field表示一个Java字段LocalVariableDecl表示局部变量Parameter表示方法或构造函数的参数
抽象语法树
此类别中的类表示抽象语法树(AST)节点,即语句(类Stmt)和表达式(类Expr)。有关标准QL库中可用的表达式和语句类型的完整列表,见用于处理Java程序的抽象语法树类
Expr和Stmt都提供了成员谓词,用于探索程序的抽象语法树:
Expr.getAChildExpr返回给定表达式的子表达式Stmt.getAChild返回直接嵌套在给定语句中的语句或表达式Expr.getParent和Stmt.getParent返回AST节点的父节点
例如,以下查询将查找所有父级为return语句的表达式:
1 | import java |
以下查询查找父级为if语句的语句(将查找程序中所有if语句的then分支和else分支):
1 | import java |
最后,这是一个查找方法体的查询:
1 | import java |
正如这些示例所示,表达式的父节点并不总是表达式:它也可能是语句,例如IfStmt。类似地,语句的父节点并不总是一个语句:它也可能是一个方法或构造函数。为了解决这个问题,QL Java库提供了两个抽象类ExprParent和StmtParent,前者表示可能是表达式父节点的任何节点,后者表示可能是语句父节点的任何节点
有关使用AST类的更多信息,见Java中容易溢出的比较运算
元数据
除了程序代码之外,Java程序还有几种元数据。特别是有注解和Javadoc注释。由于此元数据对于增强代码分析和作为分析主题本身都很有趣,因此QL库定义了用于访问它的类
对于注解,类Annotatable是所有可注解的程序元素的超类。包括包、引用类型、字段、方法、构造函数和局部变量声明。对于每一个这样的元素,其谓词getAnAnnotation能检索该元素可能具有的任何注解。例如,以下查询查找构造函数上的所有注解:
1 | import java |
这些注解由类Annotation表示。注解只是类型为AnnotationType的表达式。例如,可以修改此查询,使其只报告废弃的构造函数
1 | import java |
有关使用注解的更多信息,见Java中的注解
对于Javadoc,类Element有一个成员谓词getDoc,它返回一个委派的Documentable的对象,然后可以查询它附加的Javadoc注释。例如,以下查询在私有字段上查找Javadoc注释:
1 | import java |
类Javadoc将整个Javadoc注释表示为JavadocElement节点树,可以使用成员谓词getAChild和getParent遍历这些节点。例如,你可以编辑查询,以便在私有字段的Javadoc注释中找到所有@author标签:
1 | import java |
有关使用Javadoc的更多信息,见Javadoc
度量
标准的QL Java库为计算Java程序元素的度量提供了广泛的支持。为了避免与度量计算相关的成员谓词过多而给代表这些元素的类造成过重的负担,这些谓词被放在委托类上
总共有六个这样的类:MetricElement、MetricPackage、MetricRefType、MetricField、MetricCallable和MetricStmt。相应的元素类各自提供一个成员谓词getMetrics,可用于获取委托类的实例,然后在这个实例上进行度量计算。例如,以下查询查找圈复杂度大于40的方法:
1 | import java |
调用图
从Java代码生成的CodeQL数据库包含有关程序调用图的预计算信息,即给定调用在运行时可以分派给哪些方法或构造函数。
前文介绍的Callable类,它包括方法,也包括构造函数。调用表达式是使用类Call来进行抽象的,它包括方法调用、new表达式和使用this或super的显式构造函数调用
我们可以使用谓词 Call.getCallee 来查找一个特定的调用表达式所指向的方法或构造函数。例如,以下查询查找名为println的方法的所有调用:
1 | import java |
相反, Callable.getAReference 返回指向它的 Call 。所以我们可以使用这个查询找到从未被调用的方法或构造函数:
1 | import java |
有关可调用项和调用的更多信息,见导航调用图
Java中的数据流分析
数据流分析用于计算一个变量在程序中各个点上可能保持的值,确定这些值如何在程序中传播以及它们的使用位置
局部数据流
局部数据流是单个方法内或可调用内的数据流。局部数据流通常比全局数据流更容易、更快、更精确,并且对于许多查询来说已经足够了
使用局部数据流
局部数据流库位于DataFlow模块中,该模块定义了类Node来表示数据可以通过的任意元素。Node分为表达式节点(ExprNode)和参数节点(ParameterNode)。可以使用成员谓词asExpr和asParameter在数据流节点和表达式/参数之间映射:
1 | class Node { |
或者使用谓词exprNode和parameterNode:
1 | /** |
如果存在一条从节点nodeFrom到节点nodeTo的实时数据流边,则谓词 localFlowStep(Node nodeFrom, Node nodeTo) 成立。可以通过使用+或*运算符来递归地应用localFlowStep,或者通过使用预定义的递归谓词localFlow(相当于localFlowStep*)
例如,可以在零个或多个局部步骤中找到从参数source到表达式sink的流:
1 | DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink)) |
使用局部污点跟踪
局部污点跟踪通过包含非保值流步骤来扩展局部数据流。例如:
1 | String temp = x; |
如果x是污点字符串,那么y也是污点
局部污点跟踪库位于TaintTracking模块中。与局部数据流一样,如果存在一条从节点nodeFrom到节点nodeTo的实时污染传播边,则谓词localTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo)成立。可以使用+和*运算符递归地应用谓词,或者使用预定义的递归谓词localTaint(相当于 localTaintStep*)
例如,可以在零个或多个局部步骤中找到从参数source到表达式sink的污染传播:
1 | TaintTracking::localTaint(DataFlow::parameterNode(source), DataFlow::exprNode(sink)) |
示例
此查询查找传递给新new FileReader(..)的文件名
1 | import java |
Member::getDeclaringType:
RefType getDeclaringType(),获取定义此成员的类型
但这只给出参数中的表达式,而不是可以传递给它的值。所以我们使用局部数据流来查找流入参数的所有表达式:
1 | import java |
然后我们可以使源更加具体,例如对一个公共参数的访问。此查询查找将公共参数传递给new FileReader(..)的位置:
1 | import java |
此查询查找对格式字符串没有硬编码的格式化函数的调用
1 | import java |
exists:
exists(<variable declarations> | <formula>)。还可以写作exists(<variable declarations> | <formula 1> | <formula 2>),相当于exists(<variable declarations> | <formula 1> and <formula 2>)。这个函数引入了一些新的变量,如果变量至少有一组值可以使主体中的公式为真,则该函数成立。例如,exists(int i | i instanceof OneTwoThree)引入int类型的临时变量i,如果i的任何值是OneTwoThree类型,则函数成立StringLiteral:
Class StringLiteral,字符串文本或文本块(Java 15特性)
练习
练习1:使用局部数据流编写一个查询,查找所有用于创建 java.net.URL的硬编码字符串(答案)
全局数据流
全局数据流跟踪整个程序中的数据流,因此比局部数据流更强大。然而,全局数据流不如局部数据流精确,分析通常需要大量时间和内存
使用全局数据流
可以通过扩展类 DataFlow::Configuration来使用全局数据流库
1 | import semmle.code.java.dataflow.DataFlow |
这些谓词在配置中定义:
isSource—定义了数据可能从何而来isSink—定义了数据可能流向的位置isBarrier—可选,限制数据流isAdditionalFlowStep—可选,添加额外的流程步骤
特征谓词 MyDataFlowConfiguration() 定义了配置的名称,所以"MyDataFlowConfiguration"应该是唯一的名称,例如你的类名
使用谓词 hasFlow(DataFlow::Node source, DataFlow::Node sink)执行数据流分析:
1 | from MyDataFlowConfiguration dataflow, DataFlow::Node source, DataFlow::Node sink |
使用全局污点跟踪
就像局部污点跟踪是对局部数据流的跟踪一样,全局污点跟踪是对全局数据流的跟踪。也就是说,全局污点跟踪通过额外的非保值步骤扩展了全局数据流。
Difference between DataFlow::Configuration and TaintTracking::Configuration
可以通过扩展类 TaintTracking::Configuration来使用全局污点跟踪库:
1 | import semmle.code.java.dataflow.TaintTracking |
这些谓词在配置中定义:
isSource—定义了污点可能来自哪里isSink—定义了污点可能流向哪里isSanitizer—可选,限制污点的流动isAdditionalTaintStep—可选,添加其他污点步骤
与全局数据流类似,特征谓词 MyTaintTrackingConfiguration() 定义了配置的唯一名称
污点跟踪分析使用谓词 hasFlow(DataFlow::Node source, DataFlow::Node sink)
Flow sources
数据流库包含一些预定义的流源。 RemoteFlowSource 类(在semmle.code.java.dataflow.FlowSources)中定义)表示可能由远程用户控制的数据流源,这对于查找安全问题很有用
示例
此查询显示使用远程用户输入作为数据源的污点跟踪配置
1 | import java |
练习
练习2:编写一个查询,使用全局数据流查找用于创建 java.net.URL的所有硬编码字符串(答案)
练习3:编写一个表示来自 java.lang.System.getenv(..)的流源的类(答案)
练习4:使用2和3中的答案,编写一个查询,查找所有从 getenv 到java.net.URL的全局数据流(答案)
练习答案
练习1
1 | import semmle.code.java.dataflow.DataFlow |
练习2
1 | import semmle.code.java.dataflow.DataFlow |
练习3
1 | import java |
练习4
1 | import semmle.code.java.dataflow.DataFlow |
Java中的类型
标准CodeQL库通过Type类及其各种子类来表示Java类型
PrimitiveType类表示Java语言中内置的基本类型(如boolean和int),而RefType及其子类表示引用类型,即类、接口、数组类型等。也包括来自Java标准库的类型(如Java.lang.Object)和由非库代码定义的类型
RefType类还为类层次结构建模:成员谓词getASupertype和getASubtype可以查找引用类型的直接超类和子类。例如,对于以下Java程序:
1 | class A {} |
类A有一个直接超类(java.lang.Object)和一个直接子类(B);接口I也是如此。而类B有两个直接超类(A和I),没有直接子类
为了确定超类(包括直接超类,以及它们的超类等),我们可以使用传递闭包。例如,要在上面的示例中查找B的所有超类,我们可以使用以下查询:
1 | import java |
如果在上面的示例代码段上运行此查询,则查询将返回A、I和java.lang.Object
除了类层次结构建模,RefType还提供成员谓词getAMember,用于访问类中声明的成员(即字段、构造函数和方法),以及谓词inherits(Method m),用于检查类是否声明或继承方法m
示例:查找有问题的数组强制转换
作为如何使用类层次结构API的示例,我们可以编写一个查询来查找数组的向下转型,也就是某种类型A[]转换为类型B[]的表达式e(B是A的(不一定是直接的)子类)
这种类型的转换是有问题的,因为向下转换数组会导致运行时异常,即使每个数组元素都可以向下转换。例如,以下代码会引发ClassCastException:
1 | Object[] o = new Object[] { "Hello", "world" }; |
另一方面,如果表达式e恰好计算为B[]数组,则转换将成功:
1 | Object[] o = new String[] { "Hello", "world" }; |
在本文中,我们不尝试区分这两种情况。 我们的查询应该只是简单地查找从source类转换为target类的转换表达式ce:
source和target都是数组类型source的元素类型是target元素类型的可传递超类
1 | import java |
请注意,通过将target.getElementType() 转换为RefType,我们排除了所有元素类型为原始类型的情况,即 target是原始类型的数组:在这种情况下不会出现我们正在寻找的问题。 与 Java 不同,QL 中的强制转换永远不会失败:如果无法将表达式强制转换为所需的类型,它会简单地从查询结果中排除,这也正是我们想要的
改进
在版本5之前的旧Java代码上运行此查询,通常会返回由于使用将集合转换为T[]类型的数组的方法Collection.toArray(T[])而产生的许多误报结果
在不使用泛型的代码中,这个方法通常如下使用:
1 | List l = new ArrayList(); |
这段代码中,l是原始类型List,所以l.toArray返回Object[]类型,与它的参数数组的类型无关。因此从Object[]转到A[]会被我们的查询标记为有问题,尽管在运行时,这个转换永远不会出错
为了识别这些情况,我们可以创建两个CodeQL类分别用来表示Collection.toArray方法和此方法或任何重写它的方法的调用:
1 | /** class representing java.util.Collection.toArray(T[]) */ |
注意在CollectionToArrayCall的构造函数中使用了getSourceDeclaration和overridesOrInstantiates:我们希望找到对Collection.toArray方法和任何重写它的方法的调用,以及这些方法的任何参数化实例。例如,在上面的示例中,l.toArray解析为原始类型ArrayList中的toArray方法。其源声明是位于泛型类ArrayList<T>中的toArray,该类重写AbstractCollection<T>.toArray,这反过来会覆盖Collection<T>.toArray,它是Collection.toArray的一个实例化。(因为重写方法中的类型参数T属于ArrayList,并且是属于Collection的类型参数的实例)
使用这些新类,我们可以扩展查询,排除误报:
1 | import java |
示例:查找不匹配的contains
我们现在将编写一个查询来查找查询元素的类型与集合的元素类型无关的Collection.contains的使用
例如,Apache Zookeeper以前在类QuorumPeerConfig中有一段类似于以下内容的代码:
1 | Map<Object, Object> zkProp; |
由于zkProp是从Object到Object的映射,因此zkProp.entrySet返回一个Set<Entry<Object, Object>>类型的集合。 这样的集合不可能包含String类型的元素(代码已被修复为使用zkProp.containsKey)
一般来说,我们希望找到对Collection.contains的调用(或任何Collection的参数化实例中的重写了它方法),而且集合元素的类型E和contains参数的类型A是不相关的,也就是说,它们没有共同的子类
首先创建一个描述java.util.Collection的类:
1 | class JavaUtilCollection extends GenericInterface { |
为了确保没有错误,可以运行一个简单的测试查询:
1 | from JavaUtilCollection juc |
这个查询应该只返回一个结果
然后创建一个描述java.util.Collection.contains的类:
1 | class JavaUtilCollectionContains extends Method { |
这里使用了hasStringSignature来检查以下项:
该方法的名称为
contains它只有一个参数
参数的类型是
Object
或者可以使用hasName,getNumberOfParameters,getParameter(0).getType() instanceof TypeObject来分别实现这三项
现在我们要识别对Collection.contains的所有调用,包括任何重写它的方法,并考虑Collection的所有参数化实例以及其子类,编写如下
1 | class JavaUtilCollectionContainsCall extends MethodAccess { |
对于每次调用contains,我们关注的是参数的类型以及调用它的集合的元素类型。 所以我们需要在类JavaUtilCollectionContainsCall中添加getArgumentType和getCollectionElementType这两个成员谓词
前者很简单:
1 | Type getArgumentType() { |
对于后者,我们将按以下步骤进行:
- 找到被调用的
contains方法的声明类型D - 找到
D的超类S(或者D本身),且是java.util.Collection的参数化实例 - 返回
S的类型参数
1 | Type getCollectionElementType() { |
将这两个成员谓词添加到JavaUtilCollectionContainsCall中,我们还需要编写一个谓词来检查两个给定的引用类型是否具有公共子类:
1 | predicate haveCommonDescendant(RefType tp1, RefType tp2) { |
现在可以编写出查询的第一个版本:
1 | import java |
改进
对于很多程序来说,由于类型变量和通配符,这个查询会产生大量的误报结果:如果集合元素类型是某个类型变量 E,参数类型是String,例如 CodeQL 会认为这两者没有共同子类,我们的查询将标记调用。 排除此类误报结果的一种简单方法是简单地要求collEltType和argType都不是TypeVariable的实例
误报的另一个来源是原始类型的自动装箱:例如,如果集合的元素类型是Integer并且参数是int类型,则谓词haveCommonDescendant将失败,因为int不是 RefType。 考虑到这一点,我们的查询应该检查collEltType不是argType的装箱类型
最后null是特殊的,因为它的类型(在 CodeQL 库中称为 <nulltype>)与每个引用类型兼容,因此我们应该将其排除在考虑之外
加上这三项改进,我们的最终查询是:
1 | import java |
Java中容易溢出的比较运算
1 | void foo(long l) { |
如上代码,如果l大于2^31-1(int类型的最大正值),那么这个循环将永远不会停止:i将从零开始,一直递增到2^31-1,它仍然小于l。 当它再次递增时会发生溢出,变为-2^31
初始查询
首先编写一个查找小于表达式的查询(CodeQL 类LTExpr),其中左操作数为int类型,右操作数为long类型:
1 | import java |
使用谓词getType(可用于Expr的所有子类)来确定操作数的类型。 Type定义了hasName谓词,可以用来识别原始类型int和long。 目前而言,此查询查找所有比较int和long的小于表达式,但实际上我们只对作为循环条件一部分的比较感兴趣。 此外,我们希望过滤掉任一操作数为常数的比较,因为这些不太可能是真正的错误。 修改后的查询如下所示:
1 | import java |
LoopStmt类是所有循环的通用超类,当然也包括上面示例中的for循环。 虽然不同种类的循环有不同的语法,但它们都有一个循环条件,可以通过谓词getCondition获取。 我们使用应用于getAChildExpr谓词的自反传递闭包运算符*来表达expr应该嵌套在循环条件内的要求。 特别是,它可以是循环条件本身
where子句中的最后一个连词利用了谓词可以返回多个值这一特性。 比如getAnOperand可以返回expr的任一操作数,因此如果至少有一个操作数是常量,则expr.getAnOperand().isCompileTimeConstant()成立。 否定这个条件意味着查询只会找到两个操作数都不是常量的表达式
完善查询
当然,int和long之间的比较并不是唯一有问题的情况:窄类型和宽类型之间的任何小于比较都可能是可疑的,小于等于、大于和大于等于与小于比较一样有问题
为了比较类型的范围,我们定义了一个谓词,它返回给定整数类型的宽度(以位为单位):
1 | int width(PrimitiveType pt) { |
现在我们想将我们的查询推广应用于任何类型宽度较小的一端小于较大的一端的比较,引入一个抽象类对其进行建模:
1 | abstract class OverflowProneComparison extends ComparisonExpr { |
这个类有两个具体的子类:一个用于<=或<比较,另一个用于>=或>比较。在这两种情况下,我们实现构造函数的方式都是只匹配我们想要的表达式:
1 | class LTOverflowProneComparison extends OverflowProneComparison { |
现在,我们利用这些新类重写查询:
1 | import Java |
导航调用图
CodeQL 具有用于识别调用其他代码的代码以及可以从其他地方调用的代码的类。 例如可以用来找到从未使用过的方法
调用图类
Java 的 CodeQL 库提供了两个抽象类来表示程序的调用图:Callable 和 Call。 前者简单来说就是Method和Constructor的共同超类,后者是MethodAccess、ClassInstanceExpression、ThisConstructorInvocationStmt和SuperConstructorInvocationStmt的公共超类。 简单地说,Callable 是可以调用的东西,Call 是调用 Callable 的东西
例如,在以下程序中,所有可调用项和调用都已添加注释:
1 | class Super { |
Call类提供了两个导航调用图的谓词:
getCallee返回此调用(静态)解析为的Callable; 请注意,对于实例(即非静态)方法的调用,在运行时调用的实际方法可能是重写此方法的其他方法getCaller获取此调用的可调用对象
例如,在我们的示例中,Client.main 中第二次调用, getCallee 将返回 Super.getX。 但是,在运行时,这个调用实际上会调用 Sub.getX
类Callable定义了大量成员谓词;就我们的目的而言,最重要的两个方面是:
calls(Callable target):如果此可调用对象调用target则成立polyCalls(Callable target):如果此可调用对象可以调用指定的可调用对象则成立
在我们的示例中,Client.main calls构造函数 Sub(int) 和 Super.getX方法; 此外,它 polyCalls Sub.getX方法
示例:查找未使用的方法
我们可以使用 Callable 类编写一个查询来查找未被任何其他方法调用的方法:
1 | import java |
在这里使用 polyCalls 而不是calls:我们希望合理地确定被调用者没有被调用,无论是直接调用还是通过覆盖调用
在一般的 Java 项目上运行此查询会在 Java 标准库中产生大量命中。 因为没有一个客户端程序使用标准库的所有方法。 更一般地说,我们可能希望从编译的库中排除方法和构造函数。 我们可以使用谓词 fromSource 来检查编译单元是否是源文件,并细化我们的查询:
1 | import java |
我们可能还会注意到几个名称有点奇怪的未使用方法 <clinit>:它们是类初始化器; 虽然它们没有在代码中的任何地方显式调用,但只要加载上下文的类,它们就会被隐式调用。 因此,将它们从我们的查询中排除是有意义的。 当我们这样做时,我们还可以排除终结器,它们同样被隐式调用:
1 | import java |
我们可能还想从查询中排除公共方法,因为它们可能是外部API入口点:
1 | import java |
另一个特殊情况是非公共默认构造函数:例如,在单例模式中,为类提供了私有的空默认构造函数以防止它被实例化。 由于此类构造函数的目的是不调用它们,因此不应标记它们:
1 | import java |
最后,在许多 Java 项目中,存在通过反射间接调用的方法。 因此,虽然没有调用这些方法的调用,但它们实际上已被使用。 通常很难识别这些方法。 然而,一个非常常见的特殊情况是 JUnit 测试方法,它由测试运行程序反射调用。 Java 的 CodeQL 库支持识别 JUnit 和其他测试框架的测试类,我们可以使用它们来过滤掉这些类中定义的方法:
1 | import java |
Java中的注解
Java项目的CodeQL数据库包含所有附加到程序元素的注解信息。
注解由以下CodeQL类表示:
Annotatable表示所有可能附加注解的实体 (包,引用类型,字段,方法,局部变量)AnnotationType标识Java注解类型,例如java.lang.Override;注解类型是接口AnnotationElement表示注解元素,即注解类型的成员Annotation标识注解,例如@Override;可以通过成员谓词getValue访问注解值
例如,Java标准库定义了一个注解SuppressWarnings,指示编译器不要发出某些类型的警告:
1 | package java.lang; |
SuppressWarnings表示为AnnotationType,value是其唯一的AnnotationElement
1 | class A { |
表达式 @SuppressWarnings("rawtypes") 表示为 Annotation。字符串 "rawtypes" 用于初始化注解元素 value,其值可以通过getValue谓词从注解中提取
可以编写如下查询查找所有附加到构造函数的注解 @SuppressWarnings , 并返回注解本身及其value元素的值:
1 | import java |
另一个示例是查找所有只有一个注释元素的注释类型,该元素具有名称value:
1 | import java |
Javadoc
为了访问与程序元素相关联的Javadoc,我们使用Element类的成员谓词getDoc,它返回一个Documentable。类Documentable反过来提供了一个成员谓词getJavadoc来检索附加到相关元素(如果有的话)的Javadoc
Javadoc注释由类Javadoc表示,该类提供了作为JavadocElement节点树的注释视图。每个JavadocElement要么是一个JavadocTag,表示一个标签,要么是一个JavadocText,表示一段自由格式的文本
Javadoc类最重要的成员谓词:
getAChild- 获取树表示中的顶级JavadocElement节点getVersion- 返回@version标签的值(如果有)getAuthor- 返回@author标签的值(如果有)
例如,以下查询查找所有同时具有@author标签和@version标签的类,并返回此信息:
1 | import java |
JavadocElement定义成员谓词getAChild和getParent,以在元素树上下导航。它还提供了一个谓词getTagName来返回标签的名称,以及一个谓词getText来访问与标签关联的文本
我们可以使用这个API代替getAuthor和getVersion重写上面的查询:
1 | import java |
JavadocTag有几个子类,代表特定类型的Javadoc标签:
ParamTag表示@param标签;成员谓词getParamName返回要记录的参数的名称ThrowsTag表示@throws标签;成员谓词getExceptionName返回正在记录的异常的名称AuthorTag表示@author标签;成员谓词getAuthorName返回作者的名称