使用 Nix 對 PDF 解析器進行模糊測試(下)
原文由 Michael Lynch 于 發布,訂閱此部落格
這篇文章的下半部延續了前一篇關於使用 Nix 自動化模糊測試工作流程的主題。
到目前為止,我已經可以用 honggfuzz 來測試 pdftotext,但啟動過程還需要一些手動操作。我在第一篇曾承諾,要把安裝與模糊測試的全部流程濃縮成單一指令。
下載難以處理的 PDF
在先前的臨時模糊測試中,我是手動從美國國稅局網站下載 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.js 的儲存庫中還有數百個 .link 檔案,裡面存放著外部 PDF 的網址。
我一開始不知道該如何從這些 .link 檔案的網址下載 PDF,因為 mkDerivation 階段預設會阻擋網路存取。我雖然可以用 fetchUrl 指令,但那樣就得寫上好幾百條。
Anton Mosich 教了我一個優雅的方法,可以一次下載所有 .link 檔案中的 PDF。他告訴我,只要指定 outputHash、outputHashMode 和 outputHashAlgo,Nix 就會放寬限制,允許在建置過程中存取網路。
我一開始嘗試用 curl 逐一下載每個檔案,但速度實在太慢,因為它是循序下載。後來我找到一個叫 aria2c 的工具,可以平行下載網址,讓整個過程快上許多:
{
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 了
這樣雖然可行,但每次執行 shell 腳本前,都得記得先跑一次 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 app 後,我就擁有了一套完整的模糊測試工作流程。我可以讓模糊測試器一直執行,讓它自行找出錯誤。
用 ASAN 把隱蔽的記憶體錯誤變成明顯的當機
到目前為止,我的模糊測試工作流程已經可以運作,但還能跑得更有效率。
做模糊測試時,只有當目標應用程式當機,你才會知道自己找到了一個值得關注的錯誤。問題在於,有很多方式能讓程式行為異常,卻不一定會導致當機。
最著名的不會造成當機的安全漏洞之一,就是 2014 年 OpenSSL 的 Heartbleed 漏洞。它讓攻擊者能從網頁伺服器竊取敏感資訊,卻不會讓伺服器當機。誘使程式讀寫超出預期範圍的記憶體,並不一定會造成當機。
好消息是,有個工具能讓那些原本不會當機的記憶體錯誤立刻造成當機。Address Sanitizer(ASAN)會在程式的記憶體讀寫中加入額外的安全檢查,一旦程式試圖讀寫超出變數記憶體範圍的位置,就會立刻當機並輸出除錯資訊。
在模糊測試工作流程中加入 ASAN,能讓我找到更多原本難以發現的記憶體錯誤。要在啟用 ASAN 的情況下編譯 xpdf,我在 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 前綴的方法,是一個有點醜陋的權宜之計:在編譯時使用 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)) {這樣就能確保函式會把空字串和空指標(null pointer)同等對待,不再繼續處理。
為了驗證我的推測,我會建立一個包含這項修改的修補檔,重新編譯 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 對本文的協助。
隨機一篇部落格
留言
登入後參與討論