CodeQL数据库创建失败的排查与使用cvise生成完美复现案例指南

本文详细介绍了当CodeQL数据库创建看似成功但查询无结果时的排查步骤,包括检查构建追踪日志、定位提取器断言失败的根本原因,以及使用cvise工具自动生成最小化复现案例并提交有效错误报告的方法。

当创建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 提取器仍可能在其自身前端中遇到错误! ↩

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计