Skip to content

Commit 3f52e97

Browse files
Fix libFuzzer array alignment with empty modules (#159661)
The following assertion was being triggered: ``` assert.h assertion failed at llvm-project/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp:237 in void fuzzer::TracePC::UpdateObservedPCs(): M.Size() == (size_t)(ModulePCTable[i].Stop - ModulePCTable[i].Start) ``` # The bug When built with `-fsanitize=fuzzer`, each “module” (.so file, or the binary itself) will be instrumented, and when loaded into the process will make a call to these two functions: - `__sanitizer_cov_8bit_counters_init` - `__sanitizer_cov_pcs_init` Each of these is called with start and end pointers defining an array. In libFuzzer, these functions are implemented with `HandleInline8bitCountersInit` and `HandlePCsInit`. Each of them pushes back the provided pointers into a separate array, `Modules` and `ModulePCTable` respectively. These arrays are meant to be kept in-sync; index i into Modules should refer to the same `.so` as index i into ModulePCTable. The assertion was triggering because these lists got out-of-sync. The problem is that the 8bit handler contains this line: ``` if (Start == Stop) return; ``` but the PC handler contains no such corresponding line. This meant that if a module was ever instrumented but “empty” (its 8bit counter and PC arrays were both of length 0), then its PC array would still be added but its 8bit counter array would not. # Why this issue was never seen before The circumstances to trigger this issue are unusual: - You need a compilation unit that doesn't contain any code (though it may contain global variable declarations and similar). That doesn't happen very often. - That compilation unit must be dynamically linked, not statically linked. If statically linked, it’ll be merged into a single “module” with the main binary, and the arrays will be merged as well; you won’t end up with length-0 arrays. - To notice the issue, assertions must be enabled. If disabled, libFuzzer will be buggy (it may have worse coverage), but it won't crash, and "worse coverage" is extremely unlikely to be noticed. # This change This change solves the issue by adding the same `if (Start == Stop) return;` check to `HandlePCsInit`. This prevents the arrays from getting out-of-sync. This change also adds a test that identifies the previous issue when compiled with assertions enabled, but now passes with the fix.
1 parent 725d9e8 commit 3f52e97

File tree

3 files changed

+70
-0
lines changed

3 files changed

+70
-0
lines changed

compiler-rt/lib/fuzzer/FuzzerTracePC.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ void TracePC::HandleInline8bitCountersInit(uint8_t *Start, uint8_t *Stop) {
6969
}
7070

7171
void TracePC::HandlePCsInit(const uintptr_t *Start, const uintptr_t *Stop) {
72+
if (Start == Stop) {
73+
return;
74+
}
7275
const PCTableEntry *B = reinterpret_cast<const PCTableEntry *>(Start);
7376
const PCTableEntry *E = reinterpret_cast<const PCTableEntry *>(Stop);
7477
if (NumPCTables && ModulePCTable[NumPCTables - 1].Start == B) return;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2+
// See https://llvm.org/LICENSE.txt for license information.
3+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4+
5+
// Like SimpleTest, but simulates an "empty" module (i.e. one without any functions to instrument).
6+
// This reproduces a previous bug (when libFuzzer is compiled with assertions enabled).
7+
8+
#include <assert.h>
9+
#include <cstddef>
10+
#include <cstdint>
11+
#include <cstdlib>
12+
#include <iostream>
13+
#include <ostream>
14+
15+
extern "C" {
16+
void __sanitizer_cov_8bit_counters_init(uint8_t *Start, uint8_t *Stop);
17+
void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg,
18+
const uintptr_t *pcs_end);
19+
}
20+
21+
void dummy_func() {}
22+
23+
uint8_t empty_8bit_counters[0];
24+
uintptr_t empty_pcs[0];
25+
26+
uint8_t fake_8bit_counters[1] = {0};
27+
uintptr_t fake_pcs[2] = {reinterpret_cast<uintptr_t>(&dummy_func),
28+
reinterpret_cast<uintptr_t>(&dummy_func)};
29+
30+
// Register two modules at program launch (same time they'd normally be registered).
31+
// Triggering the bug requires loading an empty module, then a non-empty module after it.
32+
bool dummy = []() {
33+
// First, simulate loading an empty module.
34+
__sanitizer_cov_8bit_counters_init(empty_8bit_counters, empty_8bit_counters);
35+
__sanitizer_cov_pcs_init(empty_pcs, empty_pcs);
36+
37+
// Next, simulate loading a non-empty module.
38+
__sanitizer_cov_8bit_counters_init(fake_8bit_counters,
39+
fake_8bit_counters + 1);
40+
__sanitizer_cov_pcs_init(fake_pcs, fake_pcs + 2);
41+
42+
return true;
43+
}();
44+
45+
static volatile int Sink;
46+
47+
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
48+
assert(Data);
49+
if (Size > 0 && Data[0] == 'H') {
50+
Sink = 1;
51+
if (Size > 1 && Data[1] == 'i') {
52+
Sink = 2;
53+
if (Size > 2 && Data[2] == '!') {
54+
std::cout << "BINGO; Found the target, exiting\n" << std::flush;
55+
exit(0);
56+
}
57+
}
58+
}
59+
return 0;
60+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHECK: BINGO
2+
RUN: %cpp_compiler %S/SimulateEmptyModuleTest.cpp -o %t-SimulateEmptyModuleTest
3+
4+
RUN: not %run %t-SimulateEmptyModuleTest 2>&1 | FileCheck %s
5+
6+
# only_ascii mode. Will perform some minimal self-validation.
7+
RUN: not %run %t-SimulateEmptyModuleTest -only_ascii=1 2>&1

0 commit comments

Comments
 (0)