当创建CodeQL数据库失败时该怎么办 – 以及如何使用cvise报告完美的复现案例
最近,一位同事试图为 monad 项目的特定版本创建 CodeQL 数据库以执行一些安全分析。
在数据库创建过程中,一切似乎都正常。构建成功,CodeQL 没有报告任何错误,数据库也创建成功了。
然而,当尝试查询数据库时,明显有问题。
问题所在
我的同事想在数据库中查找一个特定的类。即使是一个简单的查询,用来选择所有位于特定文件夹中有位置信息的内容,也未能返回任何结果:
1
2
3
4
5
|
import cpp
from Element e
where e.getLocation().getFile().getAbsolutePath().matches("%transaction%")
select e
|
这本来应该返回一些结果,但却什么也没有返回。数据库明显出了问题。
查看构建追踪日志
当 CodeQL 数据库像这样静默创建失败时,首先要检查的是构建追踪日志。此日志包含构建过程中发生的详细信息,可以揭示那些不立即明显的问题。
构建追踪日志位于 CodeQL 数据库目录中的 $DB/log/build-tracer.log。
如果我们打开这个文件并滚动浏览,会注意到一些令人担忧的情况:许多“灾难性错误”。
1
2
3
4
|
[T 00:45:26 93] CodeQL CLI version 2.23.2
[T 00:45:26 93] Initializing tracer.
...
64 errors and 1 catastrophic error detected in the compilation of "/app/monad/category/execution/ethereum/core/transaction.cpp".
|
日志显示了许多被追踪的编译过程,但也检测到 129 个编译过程中的灾难性错误!
如果一个编译单元灾难性地失败,提取器就无法从中提取任何信息,这解释了为什么我们的查询没有返回结果。
要找出导致灾难性错误的原因,我们需要从看到灾难性失败的地方向上滚动一点,寻找实际的错误信息。
找到根本原因
浏览构建追踪日志后,我们最终找到了如下所示的错误信息:
1
|
error: assertion failed at: "decls.c", line 18401 in add_src_seq_end_of_variable_if_needed
|
这就是确凿的证据!CodeQL C/C++ 提取器在处理某些源文件时遇到了内部断言失败¹。当这种情况发生时,提取器无法从该编译单元提取任何信息,这解释了为什么我们的查询没有返回结果。
该错误指向 CodeQL 提取器内部代码的特定文件(decls.c)和行号(18401),断言在此处失败。虽然我们无法直接修复提取器,但我们可以创建一个最小的复现案例,向 CodeQL 团队报告此错误。
使用 cvise 创建最小复现案例
当向 CodeQL 团队(或任何编译器/静态分析工具团队)报告错误时,提供一个最小的复现案例非常有价值。与其要求他们克隆并构建整个 monad 项目,我们可以使用一个名为 cvise 的工具(或其前身 C-Reduce)来自动将我们的失败测试用例缩减到一个最小的示例。
什么是 cvise?
cvise 是一个用于缩减 C/C++ 程序的工具。它接收一个触发错误的大型程序,并自动移除代码,同时确保错误仍然能够复现。结果是得到一个更易于理解和调试的最小化测试用例。
对于这个目的,我强烈推荐 cvise——它为我节省了数小时的手动缩减工作!
无论你是处理编译器崩溃、静态分析工具错误,还是任何其他 C/C++ 代码问题,cvise 都是你调试工具库中不可或缺的工具。
在许多情况下,它甚至对非 C/C++ 语言(如 JavaScript 或 Java)也相当有效,只需将它们视为纯文本文件并应用类似的缩减策略!
设置“有趣性”测试
要使用 cvise,我们需要创建一个“有趣性测试”——一个脚本,如果错误复现则返回 0(成功),否则返回非零(失败)。
以下是我们将使用的有趣性测试脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#!/bin/bash
set -e
cleanup() {
rm -rf "$mytmpdir"
}
trap cleanup EXIT
mytmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
codeql database create "$mytmpdir" --language=cpp --command="/usr/lib/llvm-19/bin/clang -std=gnu++23 -c minimal.cpp" --overwrite
cat "$mytmpdir/log/build-tracer.log" | grep 'error: assertion failed at: "decls.c", line 18401 in add_src_seq_end_of_variable_if_needed'
status=$?
exit $status
|
这个脚本:
- 创建一个用于 CodeQL 数据库的临时目录
- 尝试通过使用原始构建中相同的编译器和标志编译
minimal.cpp 来创建 CodeQL 数据库
- 在构建追踪日志中搜索我们特定的错误信息
- 如果找到错误则返回 0(成功),否则返回非零(失败)
- 完成后清理临时目录
找到导致问题的源文件
在运行 cvise 之前,我们需要确定是哪个源文件导致了问题。我们可以通过 grep 在构建追踪日志中搜索错误信息,并查看之前的编译命令来找到有问题的文件。
一旦我们确定了文件,就将其复制到 minimal.cpp,并验证我们的有趣性测试是否有效:
1
2
3
4
|
cp /path/to/monad/consensus/problematic_file.cpp minimal.cpp
chmod +x test.sh
./test.sh
echo $? # 应该打印 0
|
在我们的案例中,日志显示有问题的文件来自 GNU C++ 标准库头文件 alloc_traits.h,所以我们把该文件复制到 minimal.cpp 中。
1
2
3
4
5
6
|
CodeQL C++ extractor: Current location: /app/monad/category/vm/core/assert.cpp:62055,3
CodeQL C++ extractor: Current physical location: /usr/lib/gcc/x86_64-linux-gnu/15/../../../../include/c++/15/bits/alloc_traits.h:146,3
"/usr/lib/gcc/x86_64-linux-gnu/15/../../../../include/c++/15/bits/alloc_traits.h", line 146: internal error: assertion failed at: "decls.c", line 18401 in add_src_seq_end_of_variable_if_needed
};
^
|
运行 cvise
现在我们可以运行 cvise 来缩减文件:
1
|
cvise --n 8 test.sh minimal.cpp
|
--n 8 标志告诉 cvise 使用 8 个并行进程来加速缩减。
cvise 现在将自动尝试移除代码的各个部分——函数、语句、表达式、类型限定符等等——同时持续检查错误是否仍然复现。这个过程可能需要几分钟到几小时,具体取决于原始文件的大小。
cvise 做了什么
在缩减过程中,cvise 会:
- 尝试移除整个函数
- 尝试移除语句和表达式
- 尝试简化复杂表达式
- 尝试移除模板参数和类型限定符
- 尝试将标识符重命名为更简单的名称
- 尝试许多其他转换
在每一步,它都会运行我们的有趣性测试来验证错误是否仍然复现。如果一个转换导致错误消失,它会被恢复。如果错误仍然复现,则保留该转换。
最终结果
cvise 完成后,我们会得到一个 minimal.cpp 文件,内容可能类似这样:
1
2
3
4
5
6
|
struct __allocator_traits_base {
template < typename >
static constexpr int __can_construct_at{
# 1
};
};
|
这比原来成千上万行的代码简单多了,但它仍然能触发 CodeQL 提取器中相同的断言失败!
报告错误
现在我们有了一 个最小的复现案例,可以为 CodeQL 团队创建一个错误报告。报告应包括:
- 描述:对问题的清晰描述(“CodeQL C/C++ 提取器在此代码上因断言失败而崩溃”)
- CodeQL 版本:发生错误的版本(例如,“CodeQL CLI 版本 2.23.2”)
- 最小复现案例:缩减后的
minimal.cpp 文件
- 复现命令:触发错误的确切命令
- 预期行为:应该发生什么(“代码应成功提取”)
- 实际行为:实际发生了什么(“断言失败:error: assertion failed at: ‘decls.c’, line 18401”)
有了这些信息,CodeQL 团队可以快速复现问题、调试并创建修复。
结论
当 CodeQL 数据库创建看起来成功但查询无结果时:
- 检查
codeql-db/log/build-tracer.log 处的构建追踪日志
- 查找错误信息和断言失败
- 确定失败的源文件
- 使用
cvise 创建最小复现案例
- 报告错误并提供所有相关细节
通过遵循此过程,您可以将令人沮丧的调试体验转化为有价值的错误报告,帮助改进所有人的 CodeQL。
该错误在短短 9 天后就被修复,并在 CodeQL CLI 版本 2.23.5 中发布!
附录:用于复现问题的 Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
# syntax=docker/dockerfile:1-labs
FROM ubuntu:25.04 AS base
RUN apt update && apt upgrade -y
RUN apt update && apt install -y apt-utils
RUN apt update && apt install -y dialog
RUN apt update && apt install -y \
ca-certificates \
curl \
gnupg \
software-properties-common \
wget \
git
RUN apt update && apt install -y \
clang-19 \
gcc-15 \
g++-15
RUN apt update && apt install -y \
libarchive-dev \
libbrotli-dev \
libcap-dev \
libcli11-dev \
libgmp-dev \
libtbb-dev \
libzstd-dev
RUN git clone https://github.com/category-labs/monad/ /monad && \
cd monad && git checkout 3f1f0063468e04f48ff068d388167af1c4ab5635 && \
cp /monad/scripts/ubuntu-build/* /opt/ && rm -rf /monad
RUN /opt/install-boost.sh
RUN /opt/install-tools.sh
RUN /opt/install-deps.sh
FROM base AS codeql
WORKDIR /app
RUN apt install -y unzip libstdc++-15-dev
# 更改为 v2.23.5 (已修复) 或 v2.23.3 (有问题) 以测试不同版本
RUN curl -LO "https://github.com/github/codeql-cli-binaries/releases/download/v2.23.3/codeql-linux64.zip"
RUN unzip codeql-linux64.zip && rm codeql-linux64.zip
ENV PATH="/app/codeql:$PATH"
ENV ASMFLAGS=-march=haswell
ENV CFLAGS=-march=haswell
ENV CXXFLAGS=-march=haswell
RUN git clone --recursive https://github.com/category-labs/monad/ && cd monad && git checkout 3f1f0063468e04f48ff068d388167af1c4ab5635 && mkdir build
WORKDIR /app/monad
RUN cmake -S . -B build/ -DCMAKE_C_COMPILER=/usr/bin/clang-19 -DCMAKE_CXX_COMPILER=/usr/bin/clang++-19
RUN codeql database create codeql-db/ --language=cpp --command="cmake --build build/ --target monad -- -j" --overwrite
|
¹为什么这只在 CodeQL“编译”代码时发生?CodeQL C/C++ 提取器拦截编译过程,以提取关于命令行、宏、类型等的额外信息。在此过程中,它运行自己基于 EDG 的编译器前端。这个前端与实际用于构建代码的编译器(例如 Clang 或 GCC)是分开的,可能有自己的错误和限制。因此,即使原始代码用 Clang 或 GCC 编译良好,CodeQL 提取器仍可能在其自身前端中遇到错误! ↩