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
表示引用类型,包含如下子类:Class
Java类Interface
Java接口EnumType
Java枚举类型Array
Java数组类型
例如,以下查询查找程序中为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
返回作者的名称