使用 Nix 模糊测试 PDF 解析器(第二部分)
原文由 Michael Lynch 于 发布,订阅该博客
本文是关于使用 Nix 自动化模糊测试工作流一文的下半部分。
到目前为止,我已经可以针对 pdftotext 运行 honggfuzz,但启动过程仍需一些手动操作。我在上篇中曾承诺,要将安装与模糊测试的所有步骤精简到一条命令内完成。
下载棘手的 PDF 文件
在之前的临时模糊测试中,我是从 IRS 官网手动下载了一个 PDF。接下来我先把这一步自动化。
既然要自动化,只用一个 PDF 就有点浪费了。做模糊测试时,我的目标是准备种类尽可能丰富的 PDF,以覆盖 PDF 文件格式的各个部分。
Adobe 曾提供过一个包含各种有趣测试 PDF 的语料库,但现在已经下线了。
我找到的最好的难以解析的 PDF 集合在 Mozilla 的 pdf.js 项目中。它包含了 700 个 PDF,这些文件都曾在该工具中引发过解析缺陷,因此很可能也会让其他 PDF 解析器出错。
我在 Nix flake 中新增了一个构建步骤,用于下载 Mozilla pdf.js 项目中的所有 PDF:
{
packages = rec {
...
sample-pdfs = pkgs.stdenv.mkDerivation rec {
pname = "sample-pdfs";
version = "4.7.76";
src = pkgs.fetchzip {
url = "https://github.com/mozilla/pdf.js/archive/refs/tags/v${version}.zip";
hash = "sha256-2xt8j2xJ3Teg/uiwjbWnpR6zckdxsp3LVbfsbBc3Dco=";
};
buildCommand = ''
mkdir -p $out
cp $src/test/pdfs/*.pdf $out
'';
};到这一步,flake.nix 应该是这个样子。
我用以下命令运行新增的 sample-pdfs 构建步骤:
nix build .#sample-pdfs接着,我验证 Nix 是否成功下载了 700 个 PDF:
$ ls ./result | head -n 5
160F-2019.pdf
alphatrans.pdf
annotation-border-styles.pdf
annotation-button-widget.pdf
annotation-caret-ink.pdf
$ ls ./result | wc --lines
700这个新的构建步骤为我提供了一个由边界情况 PDF 组成的初始语料库,希望它们能触发 PDF 解析代码中那些不常走到的分支。
下载更多棘手的 PDF 文件
除了 PDF 文件本身,pdf.js 仓库中还包含数百个 .link 文件,里面存放着外部 PDF 的 URL。
我一开始不知道该如何从这些 .link 文件的 URL 下载 PDF,因为 mkDerivation 步骤会屏蔽网络访问。我倒是可以用 fetchUrl 命令,但那样就得写上几百行。
Anton Mosich 给我展示了一种优雅的方法,可以一次性下载所有 .link 文件对应的 PDF。他告诉我,只要指定 outputHash、outputHashMode、outputHashAlgo,Nix 就会放宽限制,允许在构建过程中访问网络。
我起初尝试用 curl 逐个下载文件,但速度太慢,因为它是串行下载的。后来我发现了一个叫 aria2c 的工具,可以并行下载 URL,速度快了很多:
{
packages = rec {
...
sample-pdfs = pkgs.stdenv.mkDerivation rec {
pname = "sample-pdfs";
version = "4.7.76";
src = pkgs.fetchzip {
url = "https://github.com/mozilla/pdf.js/archive/refs/tags/v${version}.zip";
hash = "sha256-2xt8j2xJ3Teg/uiwjbWnpR6zckdxsp3LVbfsbBc3Dco=";
};
nativeBuildInputs = [
pkgs.aria2
];
SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
buildPhase = ''
# Extract the URLs and filenames from .link files into an input
# file of URLs for aria2c.
url_file=$(mktemp)
for pdf in $src/test/pdfs/*.pdf.link; do
url=$(sed 's/\r$//' "$pdf")
filename=$(basename "$pdf" .link)
echo "$url" >> "$url_file"
echo " out=$filename" >> "$url_file"
done
aria2c \
--input-file="$url_file" \
--max-tries=5 \
--retry-wait=20 \
--auto-file-renaming=false \
--max-concurrent-downloads=5 \
--max-connection-per-server=1 \
--dir=.
cp $src/test/pdfs/*.pdf .
'';
installPhase = ''
mkdir -p $out
cp -r . $out
'';
# We need to specify the output hash so that Nix allows Internet
# access during the build.
outputHash = "sha256-lcPF6AQNVsXH2RIiyGZQpp5VjcaBhtolQxmbqSduCNs=";
outputHashMode = "recursive";
outputHashAlgo = "sha256";
};我用 nix build 运行更新后的 sample-pdfs 步骤:
nix build .#sample-pdfs如果查看 ./result 目录,会发现比之前只复制仓库内 PDF 文件时多了 437 个文件:
$ ls ./result | wc --lines
1137自动化模糊测试
在本系列的第一部分,我展示了如何在 Nix 开发环境中手动运行 honggfuzz。我可以通过在 Nix flake 中定义一个模糊测试器的启动命令,让这个过程变得更简单。
首先,我新增一个用于启动 honggfuzz 的 shell 脚本:
{
packages = rec {
xpdf = pkgs.stdenv.mkDerivation rec {
...
}
fuzz-xpdf = pkgs.writeShellScriptBin "fuzz-xpdf" ''
readonly CORPUS_DIR='fuzz-corpus'
mkdir -p "$CORPUS_DIR"
# Copy the source corpus into a new directory for active fuzzing.
cp --force ${sample-pdfs}/*.pdf "$CORPUS_DIR"
${pkgs.honggfuzz}/bin/honggfuzz \
--input "$CORPUS_DIR" \
--instrument \
--timeout 10 \
-- ${xpdf}/bin/pdftotext ___FILE___
'';我用 nix build 构建这个 shell 脚本:
nix build .#fuzz-xpdf随后 Nix 会在 ./result/bin/fuzz-xpdf 生成一个 bash 脚本:
$ cat ./result/bin/fuzz-xpdf
#!/nix/store/1xhds5s320nfp2022yjah1h7dpv8qqns-bash-5.2p32/bin/bash
readonly CORPUS_DIR='fuzz-corpus'
mkdir -p "$CORPUS_DIR"
# Copy the source corpus into a new directory for active fuzzing.
cp --force /nix/store/gncc6jy3cry5lwbkd2b54h1dg46wfkdc-sample-pdfs-4.7.76/*.pdf "${CORPUS_DIR}"
/nix/store/kb9vkjv4admbdixrjyanfb1i9dd3cbmm-honggfuzz-2.6/bin/honggfuzz \
--input "$CORPUS_DIR" \
--instrument \
-- /nix/store/pixq8qiqyy6iwsc4wisb1vrmgy7l1kas-xpdf-4.05/bin/pdftotext ___FILE___在这个 shell 脚本中,Nix 把所有的 Nix 变量都替换成了绝对路径。而 bash 变量(CORPUS_DIR)则保持原样,以便在脚本运行时由 bash 解析。
如果运行这个 shell 脚本,就会开启一个新的模糊测试会话:
./result/bin/fuzz-xpdf该命令会产生如下界面:

我现在可以通过启动脚本来运行 honggfuzz 了
这样虽然可行,但每次执行脚本前都得记得先运行 nix build。Nix 通过 apps 功能提供了更简单的方案。
我在 Nix flake 的 packages 段之后添加一个 apps 定义:
{
packages = rec {
...
};
apps = {
default = self.apps.${system}.fuzz-xpdf;
fuzz-xpdf = {
type = "app";
program = "${self.packages.${system}.fuzz-xpdf}/bin/fuzz-xpdf";
};
};有了 fuzz-xpdf 这个 app,我就可以用一条命令启动模糊测试:
nix run .#fuzz-xpdf我已将 fuzz-xpdf 声明为这个 flake 的默认 app,所以实际上可以用更简单的命令:
nix run到这一步,flake.nix 应该是这个样子。
至此,模糊测试工作流就完成了。
我可以把这个 Nix flake 放到一个全新的目录中,运行 nix run 后,它会自动下载所有棘手的 PDF、编译 xpdf 并开始模糊测试。我可以让 honggfuzz 一直运行下去,看看它能发现哪些崩溃。

在 Nix flake 中加入 fuzz-xpdf 应用后,我就拥有了完整的模糊测试工作流。可以让模糊测试器一直运行,让它去发现缺陷。
用 ASAN 将隐蔽的内存错误变为显眼的崩溃
到目前为止,我的模糊测试工作流已经可以正常工作,但还可以让它更高效。
在模糊测试中,只有当目标应用崩溃时,你才知道发现了一个有价值的缺陷。问题在于,有很多方式能让程序行为异常却不一定会导致崩溃。
最著名的不会导致崩溃的安全漏洞之一,就是 2014 年 OpenSSL 的 Heartbleed 漏洞。它能让攻击者从 Web 服务器中窃取敏感信息,却不会导致服务器崩溃。诱使程序读写越界内存并不总会引发崩溃。
好消息是,有一种工具可以让原本不会崩溃的内存错误立刻导致程序崩溃。Address Sanitizer(ASAN)会在程序的内存读写操作中加入额外的安全检查,一旦程序试图读写超出变量内存范围,就会立即崩溃并输出调试信息。
在模糊测试工作流中加入 ASAN,能让我发现比原来更多的内存缺陷。要让 xpdf 以启用 ASAN 的方式编译,我在 xpdf 的编译步骤中加入 -fsanitize=address:
{
{
packages = rec {
xpdf = pkgs.stdenv.mkDerivation rec {
...
preConfigure = ''
export CC=${pkgs.honggfuzz}/bin/hfuzz-clang
export CXX=${pkgs.honggfuzz}/bin/hfuzz-clang++
# Use address sanitizer (ASAN).
export CFLAGS="$CFLAGS -fsanitize=address"
export CXXFLAGS="$CXXFLAGS -fsanitize=address"
'';
};到这一步,flake.nix 应该是这个样子。
现在,我终于可以启动模糊测试器,让它帮我找缺陷了。有了 Nix flake,这只需要运行:
nix run发现第一次崩溃
我让 honggfuzz 运行了两小时,回来一看,它已经发现了第一次崩溃:

honggfuzz 将导致崩溃的 PDF 保存在了一个名为如下的文件中:
SIGABRT.PC.55555592fff5.STACK.1bb46b81df.CODE.-6.ADDR.0.INSTR.mov____%eax,%edx.fuzz
为了复现这次崩溃,我运行了以下命令:
# Specify the path to the crashing PDF.
CRASHING_PDF='SIGABRT.PC.55555592fff5.STACK.1bb46b81df.CODE.-6.ADDR.0.INSTR.mov____%eax,%edx.fuzz'
# Rebuild pdftotext in the result folder
nix build
# Run pdftotext with the crashing PDF.
./result/bin/pdftotext "${CRASHING_PDF}" /dev/null程序果然崩溃了,ASAN 报告捕获到了一次堆缓冲区溢出:
==1259902==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200002228f at pc 0x557c3230bd58 bp 0x7ffd7f070cf0 sp 0x7ffd7f070ce8
READ of size 1 at 0x60200002228f thread T0
#0 0x557c3230bd57 (/nix/store/l774c0m9kh6z7iq1jn5m31kzy77kwffc-xpdf-4.05/bin/pdftotext+0x3e3d57)
#1 0x557c3231f6ea (/nix/store/l774c0m9kh6z7iq1jn5m31kzy77kwffc-xpdf-4.05/bin/pdftotext+0x3f76ea)
#2 0x557c32327a0d (/nix/store/l774c0m9kh6z7iq1jn5m31kzy77kwffc-xpdf-4.05/bin/pdftotext+0x3ffa0d)
#3 0x557c3232733c (/nix/store/l774c0m9kh6z7iq1jn5m31kzy77kwffc-xpdf-4.05/bin/pdftotext+0x3ff33c)
#4 0x557c322bfaa7 (/nix/store/l774c0m9kh6z7iq1jn5m31kzy77kwffc-xpdf-4.05/bin/pdftotext+0x397aa7)
...
SUMMARY: AddressSanitizer: heap-buffer-overflow (/nix/store/l774c0m9kh6z7iq1jn5m31kzy77kwffc-xpdf-4.05/bin/pdftotext+0x3e3d57)
Shadow bytes around the buggy address:
0x602000022000: fa fa fd fd fa fa fd fa fa fa fd fd fa fa fd fa
0x602000022080: fa fa fd fd fa fa fd fd fa fa 00 01 fa fa fd fd
0x602000022100: fa fa 00 03 fa fa fd fa fa fa 00 00 fa fa fd fa
0x602000022180: fa fa fd fa fa fa fd fd fa fa 00 02 fa fa fd fa
0x602000022200: fa fa fd fa fa fa fd fa fa fa fd fa fa fa 00 00
=>0x602000022280: fa[fa]00 fa fa fa fd fa fa fa fd fa fa fa fd fa
0x602000022300: fa fa fd fa fa fa fd fa fa fa 03 fa fa fa fd faASAN 的错误信息有点晦涩,但它的意思是:ASAN 捕获到 pdftotext 试图在已分配的缓冲区之外读取 1 个字节。
那么,该如何进一步深挖这次崩溃的原因呢?
完善调试符号
当 pdftotext 崩溃时,我本希望看到包含源文件名和行号的堆栈跟踪。但实际输出的只是二进制偏移,这让调试变得更困难:
#0 0x557c3230bd57 (/nix/store/l774c0m9kh6z7iq1jn5m31kzy77kwffc-xpdf-4.05/bin/pdftotext+0x3e3d57)
#1 0x557c3231f6ea (/nix/store/l774c0m9kh6z7iq1jn5m31kzy77kwffc-xpdf-4.05/bin/pdftotext+0x3f76ea)
#2 0x557c32327a0d (/nix/store/l774c0m9kh6z7iq1jn5m31kzy77kwffc-xpdf-4.05/bin/pdftotext+0x3ffa0d)奇怪的是,整个过程中最难的部分,竟然是弄清楚如何让调试符号正常工作,以便在崩溃转储中看到源码信息。
首先,我偶然注意到 nix run 的日志输出中提到会从二进制文件中剥离调试信息。原来 Nix 有一个 dontStrip 选项,默认值是 false,意味着它会自动剥离调试信息。
我还注意到 xpdf 的编译说明中提到了 CMAKE_BUILD_TYPE 选项。这个选项没有文档说明,但在源码中搜索后发现它接受 Debug 这个值。
为了在 xpdf 二进制文件中保留调试符号,我在 xpdf 包定义的末尾加入了这些选项:
{
{
packages = rec {
xpdf = pkgs.stdenv.mkDerivation rec {
...
preConfigure = ''
...
'';
cmakeFlags = [
"-DCMAKE_BUILD_TYPE=Debug"
];
# Don't strip debug information from binaries, as the debug symbols
# are usefule during crash analysis.
dontStrip = true;
};这时发生了件奇怪的事。我一度得到了包含文件名和行号的详细堆栈跟踪,但几小时后它们又莫名其妙地失效了。至今我也不知道原因。
为了让详细的堆栈跟踪稳定生效,我不得不用到一个叫 llvm-symbolizer 的工具,我之前从未听说过。好在 llvm-symbolizer 是常用 llvm_18 Nix 包的一部分,所以我把这个包加入了 Nix flake,并添加了一个名为 ASAN_SYMBOLIZER_PATH 的环境变量指向该二进制文件。
对 Nix flake 的这些改动涉及文件的多个不同部分,不太好直接展示,所以直接看 diff 最为方便。
改动 Nix flake 之后,我需要进入 Nix 开发环境才能看到详细的堆栈跟踪,因为那样才会正确设置 ASAN_SYMBOLIZER_PATH 环境变量。
# Enter the nix dev shell.
nix develop
# Rebuild pdftotext in the result folder
nix build
# Specify the path to the crashing PDF.
CRASHING_PDF='SIGABRT.PC.55555592fff5.STACK.1bb46b81df.CODE.-6.ADDR.0.INSTR.mov____%eax,%edx.fuzz'
# Run pdftotext with the crashing PDF.
./result/bin/pdftotext "${CRASHING_PDF}" /dev/null然后,我终于应该能看到带文件名的堆栈跟踪了:
=================================================================
==1461608==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200002228f at pc 0x55555592fff5 bp 0x7fffffffac70 sp 0x7fffffffac68
READ of size 1 at 0x60200002228f thread T0
#0 0x55555592fff4 in GString::getChar(int) /build/source/goo/GString.h:82:32
#1 0x55555592fff4 in GfxFont::readFontDescriptor(XRef*, Dict*) /build/source/xpdf/GfxFont.cc:553:20
#2 0x5555559423da in GfxCIDFont::GfxCIDFont(XRef*, char const*, Ref, GString*, GfxFontType, Ref, Dict*) /build/source/xpdf/GfxFont.cc:1732:3
#3 0x55555594a065 in GfxFont::makeFont(XRef*, char const*, Ref, Dict*) /build/source/xpdf/GfxFont.cc:190:16
...
#22 0x7ffff7a7110d in __libc_start_call_main (/nix/store/r8qsxm85rlxzdac7988psm7gimg4dl3q-glibc-2.39-52/lib/libc.so.6+0x2a10d) (BuildId: 323d12eb412f4a20879fb07d3514ca673c5aee20)
#23 0x7ffff7a711c8 in __libc_start_main@GLIBC_2.2.5 (/nix/store/r8qsxm85rlxzdac7988psm7gimg4dl3q-glibc-2.39-52/lib/libc.so.6+0x2a1c8) (BuildId: 323d12eb412f4a20879fb07d3514ca673c5aee20)
#24 0x555555698a04 in _start (/nix/store/x59ccyx8gz0ap74zapdi7k8ssgypmipm-xpdf-4.05/bin/pdftotext+0x144a04)成功了!现在我能看到源文件名、行号和函数名了。
但还有一个问题。看看任意文件的路径。
/build/source/xpdf/GfxFont.cc
^^^^^^^^^^^^^
Where is this coming from?xpdf 的源码路径都指向一个叫 /build/source 的根目录,但我的系统上根本不存在这个路径:
$ ls /build/source
ls: cannot access '/build/source': No such file or directory我不确定 (编辑:该前缀来自 Nix 的 /build/source 这个路径是 Nix 的特殊行为,还是 xpdf 构建配置的问题。sandbox-build-dir 选项,它定义了从源码构建时的根目录。感谢 Dionysis Grigoropoulos 的澄清。)
我唯一能修复 /build/source 前缀的办法,是用一个不太优雅的 hack,在编译时通过 clang 的 -fdebug-prefix-map 标志把错误的路径替换为正确路径:
{
{
packages = rec {
xpdf = pkgs.stdenv.mkDerivation rec {
...
preConfigure = ''
...
# For some reason, without these flags, the debug symbols point to
# source files at the base filesystem /build/source, so we
# manually fix the source path.
export CXXFLAGS="$CXXFLAGS -fdebug-prefix-map=/build/source=${xpdf.src}"
'';如果重新运行 nix build 流程,堆栈跟踪终于正确了:
==1498830==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200002228f at pc 0x55555592fff5 bp 0x7fffffffac70 sp 0x7fffffffac68
READ of size 1 at 0x60200002228f thread T0
#0 0x55555592fff4 in GString::getChar(int) /nix/store/alirmx60yanq6g8ym5v3laa7ncw2h9nm-source/goo/GString.h:82:32
#1 0x55555592fff4 in GfxFont::readFontDescriptor(XRef*, Dict*) /nix/store/alirmx60yanq6g8ym5v3laa7ncw2h9nm-source/xpdf/GfxFont.cc:553:20
#2 0x5555559423da in GfxCIDFont::GfxCIDFont(XRef*, char const*, Ref, GString*, GfxFontType, Ref, Dict*) /nix/store/alirmx60yanq6g8ym5v3laa7ncw2h9nm-source/xpdf/GfxFont.cc:1732:3
#3 0x55555594a065 in GfxFont::makeFont(XRef*, char const*, Ref, Dict*) /nix/store/alirmx60yanq6g8ym5v3laa7ncw2h9nm-source/xpdf/GfxFont.cc:190:16
#4 0x55555594a065 in GfxFontDict::load(char*, GfxFontDictEntry*) /nix/store/alirmx60yanq6g8ym5v3laa7ncw2h9nm-source/xpdf/GfxFont.cc:2393:12如果把那个文件路径传给 sed,就能打印出文件内容:
$ sed -n '548,558p' /nix/store/alirmx60yanq6g8ym5v3laa7ncw2h9nm-source/xpdf/GfxFont.cc
i -= 2;
} else if (i > 7 && !strncmp(name->getCString() + i - 7, "Oblique", 7)) {
flags |= fontItalic;
i -= 7;
}
char c = name->getChar(i-1);
if (!((c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9'))) {
--i;
}到这一步,flake.nix 应该是这个样子。
理解崩溃原因
现在我有了一个可稳定复现的越界内存读取崩溃。调试所需的信息已经齐全,是时候深入源码了。
堆栈跟踪的顶部指向这一行:
// goo/GString.h
// Get <i>th character.
char getChar(int i) { return s[i]; }好,s 是一个 char 缓冲区,所以它很可能存放的是 C 风格字符串。getChar 函数没有做任何边界检查来确保调用者传入的 i 是合法值,因此该函数会读取 s 所分配缓冲区之外的内存。
我回退一层,看看崩溃前 getChar 是如何被调用的。堆栈跟踪的下一行指向这里:
// xpdf/GfxFont.cc
void GfxFont::readFontDescriptor(XRef *xref, Dict *fontDict) {
...
// scan font name for bold/italic tags and update the flags
if (name) {
i = name->getLength();
if (i > 2 && !strncmp(name->getCString() + i - 2, "MT", 2)) {
i -= 2;
}
...
char c = name->getChar(i-1); // <<< CRASH好吧,这其实是一个相当简单的缺陷。
readFontDescriptor 检查了 name 不为 NULL,但它假定其长度至少为 1。如果 name 是空字符串(长度为 0),那么 getChar 调用就会变成 name->getChar(-1)。于是 getChar 返回 s[-1],也就是 s 所分配内存缓冲区之前的 1 个字节。
崩溃时的调试输出也印证了我的推测:
==241578==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200002360f at pc 0x55555592fff5 bp 0x7fffffffa650 sp 0x7fffffffa648
READ of size 1 at 0x60200002360f thread T0
#0 0x55555592fff4 in GString::getChar(int) /nix/store/alirmx60yanq6g8ym5v3laa7ncw2h9nm-source/goo/GString.h:82:32
#1 0x55555592fff4 in GfxFont::readFontDescriptor(XRef*, Dict*) /nix/store/alirmx60yanq6g8ym5v3laa7ncw2h9nm-source/xpdf/GfxFont.cc:553:20
...
SUMMARY: AddressSanitizer: heap-buffer-overflow /nix/store/alirmx60yanq6g8ym5v3laa7ncw2h9nm-source/goo/GString.h:82:32 in GString::getChar(int)
Shadow bytes around the buggy address:
...
=>0x602000023600: fa[fa]00 fa fa fa fd fd fa fa fd fa fa fa fd faASAN 提示这是一次大小为 1 的非法读取,这很合理,因为 getChar 正是尝试读取单个 1 字节的字符。
ASAN 还展示了发生非法读取时的内存布局,非法地址上的字节是 fa,它紧挨在包含 00 的地址之前。变量 s 存放的是空字符串,在内存中 C++ 将其表示为 00,因此 pdftotext 试图读取空字符串之前的那个字节。这次读取是非法的,因为那块内存并非分配给变量 s 的。
修复缺陷
如果我的推测正确,这个缺陷应该很容易修复。在 readFontDescriptor 中,我可以把这一行:
if (name) {改成这样:
if (name && (name->getLength() > 0)) {这样就能确保函数将空字符串与空指针同等对待,不再继续处理。
为了验证推测,我将基于这处改动创建一个补丁,重新编译 xpdf,然后用同一个 PDF 再跑一次 pdftotext,看看修复是否能避免同样的崩溃。
xpdf 作为一个开源项目有点特别,它不发布 git 仓库,只定期发布 tarball 压缩包。不过没关系。我可以自己创建一个临时的 git 仓库,作为编辑代码的工作区:
ORIGINAL_SRC="$(nix eval --raw .#xpdf.src.outPath)"
MODIFIED_SRC="$(mktemp --directory)"
pushd "${MODIFIED_SRC}" && \
cp --recursive --verbose $ORIGINAL_SRC/* . && \
chmod -R u+w . && \
git init && \
git add --all && \
git commit --message "Dummy base commit"现在,我在一个全新的 git 仓库中有了一份 xpdf 源码的副本。我来编辑 GfxFont.cc 以完成修复:
vim xpdf/GfxFont.cc编辑完成并保存文件后,我调用 git diff 生成补丁文件,内容如下:
diff --git a/xpdf/GfxFont.cc b/xpdf/GfxFont.cc
index c3db4e8..7074354 100644
--- a/xpdf/GfxFont.cc
+++ b/xpdf/GfxFont.cc
@@ -535,7 +535,7 @@ void GfxFont::readFontDescriptor(XRef *xref, Dict *fontDict) {
obj1.free();
// scan font name for bold/italic tags and update the flags
- if (name) {
+ if (name && (name->getLength() > 0)) {
i = name->getLength();
if (i > 2 && !strncmp(name->getCString() + i - 2, "MT", 2)) {
i -= 2;我把补丁复制回模糊测试目录:
# Create a patch file for the fix.
git diff > check-font-name-length.patch
# Get back to fuzz-xpdf git repo.
popd
mv "${MODIFIED_SRC}/check-font-name-length.patch" .把补丁文件放到与 flake.nix 同一目录后,我更新 Nix flake,让它在编译 xpdf 时应用我的自定义补丁:
{
xpdf = pkgs.stdenv.mkDerivation rec {
...
src = pkgs.fetchzip {
url = "https://dl.xpdfreader.com/${pname}-${version}.tar.gz";
extension = "tar.gz";
};
# Add a custom patch to fix the font name length bug.
patches = [
./check-font-name-length.patch
];到这一步,flake.nix 应该是这个样子。
我已经准备好测试修复了。我用补丁重新编译 xpdf,并用之前导致崩溃的 PDF 来运行:
# Specify the path to the crashing PDF.
$ CRASHING_PDF='SIGABRT.PC.55555592fff5.STACK.1bb46b81df.CODE.-6.ADDR.0.INSTR.mov____%eax,%edx.fuzz'
# Rebuild pdftotext in the result folder
$ nix build
# Run pdftotext with the crashing PDF.
$ ./result/bin/pdftotext "${CRASHING_PDF}"
Syntax Error: Couldn't read xref table
Syntax Warning: PDF file is damaged - attempting to reconstruct xref table...
Syntax Error (2800): Bad dynamic code table in flate stream
Syntax Error (2800): Bad block header in flate stream
Syntax Error (2158): Dictionary key must be a name object
Syntax Error: Unterminated string
Syntax Error: Leftover args in content stream瞧!pdftotext 报告了很多错误,但程序再也没有崩溃。修复生效了。
找到修复方案后我才发现,早在几周前就有人报告过同一个漏洞,但这次实践仍然很有价值。
总结
我发现 Nix 是创建模糊测试工作流的优秀工具。
虽然要搞清楚 Nix 在模糊测试中的各种细节花了一些功夫,但现在要换成不同的 PDF 解析器或不同的模糊测试器应该很容易。Nix 的美妙之处在于它的可组合性强,因此在工作流中替换不同组件非常方便。
这个项目最终成为深入学习模糊测试和 Nix 的绝佳方式。过去一年我一直在断断续续地尝试 Nix,而以这种方式使用 Nix 帮助我厘清了许多原本模糊的概念。
源代码
我的模糊测试工作流的完整源码可在 Gitlab 上获取:
摘自 xpdf 的内容依据 GPLv3 许可证使用。感谢 Anton Mosich 对本文的帮助。
随机一篇博客
评论
登录后参与讨论