Skip to content

Commit fcc4398

Browse files
committed
feat(fuzzer): add module-scoped coverage counter regions
Each ESM module will need its own coverage counter buffer, independent of the shared global coverage map used by CJS modules. libFuzzer supports multiple disjoint counter regions, so we add a C++ function that registers per-module buffers and a TypeScript API to allocate them. The GC-prevention array in CoverageTracker keeps module buffers alive for the process lifetime, matching libFuzzer's expectation that counter memory remains valid.
1 parent d422132 commit fcc4398

5 files changed

Lines changed: 51 additions & 0 deletions

File tree

packages/fuzzer/addon.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export type StartFuzzingAsyncFn = (
4040
type NativeAddon = {
4141
registerCoverageMap: (buffer: Buffer) => void;
4242
registerNewCounters: (oldNumCounters: number, newNumCounters: number) => void;
43+
registerModuleCounters: (buffer: Buffer) => void;
4344

4445
traceUnequalStrings: (
4546
hookId: number,

packages/fuzzer/coverage.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export class CoverageTracker {
2222
private readonly coverageMap: Buffer;
2323
private currentNumCounters: number;
2424

25+
// Per-module counter buffers registered independently with libFuzzer.
26+
// We must prevent GC from reclaiming these while libFuzzer still
27+
// monitors the underlying memory.
28+
private readonly moduleCounters: Buffer[] = [];
29+
2530
constructor() {
2631
this.coverageMap = Buffer.alloc(CoverageTracker.MAX_NUM_COUNTERS, 0);
2732
this.currentNumCounters = CoverageTracker.INITIAL_NUM_COUNTERS;
@@ -65,6 +70,18 @@ export class CoverageTracker {
6570
readCounter(edgeId: number): number {
6671
return this.coverageMap.readUint8(edgeId);
6772
}
73+
74+
/**
75+
* Allocate an independent counter buffer for a single module and
76+
* register it with libFuzzer as a new coverage region. This lets
77+
* each ESM module own its own counters without sharing global IDs.
78+
*/
79+
createModuleCounters(size: number): Buffer {
80+
const buf = Buffer.alloc(size, 0);
81+
this.moduleCounters.push(buf);
82+
addon.registerModuleCounters(buf);
83+
return buf;
84+
}
6885
}
6986

7087
export const coverageTracker = new CoverageTracker();

packages/fuzzer/shared/callbacks.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ void RegisterCallbackExports(Napi::Env env, Napi::Object exports) {
2121
Napi::Function::New<RegisterCoverageMap>(env);
2222
exports["registerNewCounters"] =
2323
Napi::Function::New<RegisterNewCounters>(env);
24+
exports["registerModuleCounters"] =
25+
Napi::Function::New<RegisterModuleCounters>(env);
2426
exports["traceUnequalStrings"] =
2527
Napi::Function::New<TraceUnequalStrings>(env);
2628
exports["traceStringContainment"] =

packages/fuzzer/shared/coverage.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,33 @@ void RegisterNewCounters(const Napi::CallbackInfo &info) {
8989
__sanitizer_cov_pcs_init((uintptr_t *)(gPCEntries + old_num_counters),
9090
(uintptr_t *)(gPCEntries + new_num_counters));
9191
}
92+
93+
// Monotonically increasing fake PC so that each module's counters get
94+
// unique program-counter entries that don't collide with the shared
95+
// coverage map or with each other.
96+
static uintptr_t gNextModulePC = 0x10000000;
97+
98+
// Register an independent coverage counter region for a single ES module.
99+
// libFuzzer supports multiple disjoint counter regions; each call here
100+
// hands it a fresh one.
101+
void RegisterModuleCounters(const Napi::CallbackInfo &info) {
102+
if (info.Length() != 1 || !info[0].IsBuffer()) {
103+
throw Napi::Error::New(info.Env(),
104+
"Need one argument: a Buffer of 8-bit counters");
105+
}
106+
107+
auto buf = info[0].As<Napi::Buffer<uint8_t>>();
108+
auto size = buf.Length();
109+
if (size == 0) {
110+
return;
111+
}
112+
113+
auto *pcEntries = new PCTableEntry[size];
114+
for (std::size_t i = 0; i < size; ++i) {
115+
pcEntries[i] = {gNextModulePC++, 0};
116+
}
117+
118+
__sanitizer_cov_8bit_counters_init(buf.Data(), buf.Data() + size);
119+
__sanitizer_cov_pcs_init(reinterpret_cast<uintptr_t *>(pcEntries),
120+
reinterpret_cast<uintptr_t *>(pcEntries + size));
121+
}

packages/fuzzer/shared/coverage.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@
1717

1818
void RegisterCoverageMap(const Napi::CallbackInfo &info);
1919
void RegisterNewCounters(const Napi::CallbackInfo &info);
20+
void RegisterModuleCounters(const Napi::CallbackInfo &info);

0 commit comments

Comments
 (0)