Skip to content

Commit 6ed295c

Browse files
committed
fix(libafl): support lazy ESM coverage
Allocate ESM counters from the shared global map and let libAFL observe its active length instead of a startup snapshot. This keeps lazy import() coverage deterministic via the existing edge ID strategy and locks in the behavior with integration tests. Also ignore host Babel config for runtime transforms so ESM instrumentation keeps module semantics intact.
1 parent bba7494 commit 6ed295c

15 files changed

Lines changed: 288 additions & 146 deletions

File tree

packages/fuzzer/addon.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ export type StartLibAflAsyncFn = (
6161
type NativeAddon = {
6262
registerCoverageMap: (buffer: Buffer) => void;
6363
registerNewCounters: (oldNumCounters: number, newNumCounters: number) => void;
64-
registerModuleCounters: (buffer: Buffer) => void;
6564

6665
traceUnequalStrings: (
6766
hookId: number,

packages/fuzzer/coverage.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,23 @@
1616

1717
import { addon } from "./addon";
1818

19+
type CoverageRangeAllocator = (filename: string, edgeCount: number) => number;
20+
21+
function getCoverageRangeAllocator(): CoverageRangeAllocator {
22+
const allocator = (globalThis as Record<string, unknown>)
23+
.__jazzer_reserveCoverageRange;
24+
if (typeof allocator !== "function") {
25+
throw new Error("Coverage range allocator was not initialized");
26+
}
27+
return allocator as CoverageRangeAllocator;
28+
}
29+
1930
export class CoverageTracker {
2031
private static readonly MAX_NUM_COUNTERS: number = 1 << 20;
2132
private static readonly INITIAL_NUM_COUNTERS: number = 1 << 9;
2233
private readonly coverageMap: Buffer;
2334
private currentNumCounters: number;
2435

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-
3036
constructor() {
3137
this.coverageMap = Buffer.alloc(CoverageTracker.MAX_NUM_COUNTERS, 0);
3238
this.currentNumCounters = CoverageTracker.INITIAL_NUM_COUNTERS;
@@ -71,16 +77,17 @@ export class CoverageTracker {
7177
return this.coverageMap.readUint8(edgeId);
7278
}
7379

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;
80+
createModuleCounters(filename: string, edgeCount: number): Buffer {
81+
if (!Number.isInteger(edgeCount) || edgeCount < 0) {
82+
throw new Error(`Invalid edge count: ${edgeCount}`);
83+
}
84+
if (edgeCount === 0) {
85+
return Buffer.alloc(0);
86+
}
87+
88+
const firstEdgeId = getCoverageRangeAllocator()(filename, edgeCount);
89+
this.enlargeCountersBufferIfNeeded(firstEdgeId + edgeCount - 1);
90+
return this.coverageMap.subarray(firstEdgeId, firstEdgeId + edgeCount);
8491
}
8592
}
8693

packages/fuzzer/libafl_runtime.cpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -593,20 +593,23 @@ ParsedRuntimeOptions ParseRuntimeOptions(Napi::Env env,
593593

594594
JazzerLibAflRuntimeSharedMaps SharedMapsForRuntime(Napi::Env env) {
595595
auto *edges = CoverageCounters();
596-
const auto edges_len = CoverageCountersSize();
596+
const auto edges_capacity = CoverageCountersCapacity();
597+
auto *edges_size = CoverageCountersSizePointer();
597598
auto *cmp = CompareFeedbackMap();
598599
const auto cmp_len = CompareFeedbackMapSize();
599600
auto *compare_log = CompareLog();
600601
auto *finding_info = &gFindingInfo;
601602

602-
if (edges == nullptr || edges_len == 0 || cmp == nullptr || cmp_len == 0 ||
603-
compare_log == nullptr || finding_info == nullptr) {
603+
if (edges == nullptr || edges_capacity == 0 || edges_size == nullptr ||
604+
cmp == nullptr || cmp_len == 0 || compare_log == nullptr ||
605+
finding_info == nullptr) {
604606
throw Napi::Error::New(
605607
env,
606608
"Coverage maps were not initialized before the LibAFL backend started");
607609
}
608610

609-
return {edges, edges_len, cmp, cmp_len, compare_log, finding_info};
611+
return {edges, edges_capacity, edges_size, cmp,
612+
cmp_len, compare_log, finding_info};
610613
}
611614

612615
bool CollectRegressionCorpusFiles(

packages/fuzzer/libafl_runtime.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ struct JazzerLibAflRuntimeOptions {
4444

4545
struct JazzerLibAflRuntimeSharedMaps {
4646
uint8_t *edges;
47-
size_t edges_len;
47+
size_t edges_capacity;
48+
size_t *edges_size;
4849
uint8_t *cmp;
4950
size_t cmp_len;
5051
JazzerLibAflCompareLog *compare_log;

packages/fuzzer/rust/src/lib.rs

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::fs;
88
use std::io::IsTerminal;
99
use std::path::PathBuf;
1010
use std::rc::Rc;
11+
use std::slice;
1112
use std::time::{Duration, Instant};
1213

1314
use libafl::{
@@ -26,7 +27,9 @@ use libafl::{
2627
havoc_mutations::havoc_mutations, scheduled::HavocScheduledMutator, tokens_mutations,
2728
I2SRandReplace, Tokens,
2829
},
29-
observers::{CanTrack, HitcountsMapObserver, StdMapObserver},
30+
observers::{
31+
CanTrack, HitcountsMapObserver, StdMapObserver, VariableMapObserver,
32+
},
3033
schedulers::{
3134
powersched::PowerSchedule, IndexesLenTimeMinimizerScheduler, PowerQueueScheduler,
3235
},
@@ -82,7 +85,8 @@ pub struct JazzerLibAflRuntimeOptions {
8285
#[repr(C)]
8386
pub struct JazzerLibAflRuntimeSharedMaps {
8487
pub edges: *mut u8,
85-
pub edges_len: usize,
88+
pub edges_capacity: usize,
89+
pub edges_size: *mut usize,
8690
pub cmp: *mut u8,
8791
pub cmp_len: usize,
8892
pub compare_log: *mut JazzerLibAflCompareLog,
@@ -619,23 +623,40 @@ fn clear_finding_info(ptr: *mut JazzerLibAflFindingInfo) {
619623
}
620624
}
621625

626+
fn edge_map_len(maps: &JazzerLibAflRuntimeSharedMaps) -> usize {
627+
if maps.edges_size.is_null() {
628+
0
629+
} else {
630+
unsafe { (*maps.edges_size).min(maps.edges_capacity) }
631+
}
632+
}
633+
634+
fn has_non_zero_coverage(ptr: *mut u8, len: usize) -> bool {
635+
if ptr.is_null() || len == 0 {
636+
return false;
637+
}
638+
639+
unsafe { slice::from_raw_parts(ptr, len).iter().any(|slot| *slot != 0) }
640+
}
641+
622642
fn ensure_non_empty_edge_map(ptr: *mut u8, len: usize) -> bool {
643+
if has_non_zero_coverage(ptr, len) {
644+
return false;
645+
}
646+
623647
if ptr.is_null() || len == 0 {
624648
return false;
625649
}
626650

627651
unsafe {
628-
let map = std::slice::from_raw_parts_mut(ptr, len);
629-
if map.iter().all(|slot| *slot == 0) {
630-
// Power scheduling rejects corpus entries that never hit any edge.
631-
// Preserve the old behavior for uninstrumented callbacks by marking
632-
// one synthetic edge only when the target left the map untouched.
633-
map[0] = 1;
634-
return true;
635-
}
652+
let map = slice::from_raw_parts_mut(ptr, len);
653+
// Power scheduling rejects corpus entries that never hit any edge.
654+
// Preserve the old behavior for uninstrumented callbacks by marking
655+
// one synthetic edge only when the target left every coverage region untouched.
656+
map[0] = 1;
636657
}
637658

638-
false
659+
true
639660
}
640661

641662
unsafe fn parse_corpus_directories(options: &JazzerLibAflRuntimeOptions) -> Option<Vec<PathBuf>> {
@@ -713,7 +734,8 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run(
713734
let options = &*options;
714735
let maps = &*maps;
715736
if maps.edges.is_null()
716-
|| maps.edges_len == 0
737+
|| maps.edges_capacity == 0
738+
|| maps.edges_size.is_null()
717739
|| maps.cmp.is_null()
718740
|| maps.cmp_len == 0
719741
|| maps.compare_log.is_null()
@@ -749,11 +771,14 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run(
749771
let (monitor, monitor_state) = LibAflMonitor::new(maps.finding_info);
750772
let mut mgr = SimpleEventManager::new(monitor);
751773

752-
let edges_observer = HitcountsMapObserver::new(StdMapObserver::from_mut_ptr(
753-
"edges",
754-
maps.edges,
755-
maps.edges_len,
756-
))
774+
let edges_observer = HitcountsMapObserver::new(
775+
VariableMapObserver::from_mut_ptr(
776+
"edges",
777+
maps.edges,
778+
maps.edges_capacity,
779+
maps.edges_size,
780+
),
781+
)
757782
.track_indices();
758783
let cmp_observer =
759784
HitcountsMapObserver::new(StdMapObserver::from_mut_ptr("cmp", maps.cmp, maps.cmp_len));
@@ -814,7 +839,7 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run(
814839
let timeout_found = Cell::new(false);
815840

816841
let mut harness = |input: &BytesInput| {
817-
clear_shared_map(maps.edges, maps.edges_len);
842+
clear_shared_map(maps.edges, edge_map_len(maps));
818843
clear_shared_map(maps.cmp, maps.cmp_len);
819844
clear_compare_log(maps.compare_log);
820845
clear_finding_info(maps.finding_info);
@@ -823,7 +848,10 @@ pub unsafe extern "C" fn jazzer_libafl_runtime_run(
823848
let bytes = bytes.as_slice();
824849
let size = bytes.len().min(options.max_len);
825850
let status = unsafe { execute_one(user_data, bytes.as_ptr(), size) };
826-
let synthetic_edges = ensure_non_empty_edge_map(maps.edges, maps.edges_len);
851+
let synthetic_edges = ensure_non_empty_edge_map(
852+
maps.edges,
853+
edge_map_len(maps),
854+
);
827855
monitor_state.borrow_mut().last_edges_are_synthetic = synthetic_edges;
828856
match status {
829857
EXECUTION_CONTINUE => ExitKind::Ok,

packages/fuzzer/shared/callbacks.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ 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);
2624
exports["traceUnequalStrings"] =
2725
Napi::Function::New<TraceUnequalStrings>(env);
2826
exports["traceStringContainment"] =

packages/fuzzer/shared/coverage.cpp

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg,
2525

2626
namespace {
2727
// Shared coverage counter buffer populated from JavaScript using Buffer.
28-
// Individual slices are registered with libFuzzer by RegisterNewCounters.
28+
// It is preallocated on the JavaScript side; registerNewCounters grows the
29+
// active prefix that the fuzzing backends should observe.
2930
uint8_t *gCoverageCounters = nullptr;
31+
std::size_t gCoverageCountersCapacity = 0;
3032
std::size_t gCoverageCountersSize = 0;
3133

3234
// PC-Table is used by libFuzzer to keep track of program addresses
@@ -78,6 +80,7 @@ void RegisterCoverageMap(const Napi::CallbackInfo &info) {
7880
auto buf = info[0].As<Napi::Buffer<uint8_t>>();
7981

8082
gCoverageCounters = reinterpret_cast<uint8_t *>(buf.Data());
83+
gCoverageCountersCapacity = buf.Length();
8184
}
8285

8386
void RegisterNewCounters(const Napi::CallbackInfo &info) {
@@ -98,6 +101,10 @@ void RegisterNewCounters(const Napi::CallbackInfo &info) {
98101
info.Env(),
99102
"new_num_counters must not be smaller than old_num_counters");
100103
}
104+
if (static_cast<std::size_t>(new_num_counters) > gCoverageCountersCapacity) {
105+
throw Napi::Error::New(info.Env(),
106+
"new_num_counters exceeds the coverage map size");
107+
}
101108
if (new_num_counters == old_num_counters) {
102109
return;
103110
}
@@ -107,28 +114,14 @@ void RegisterNewCounters(const Napi::CallbackInfo &info) {
107114
gCoverageCountersSize = static_cast<std::size_t>(new_num_counters);
108115
}
109116

110-
// Register an independent coverage counter region for a single ES module.
111-
// libFuzzer supports multiple disjoint counter regions; each call here
112-
// hands it a fresh one.
113-
void RegisterModuleCounters(const Napi::CallbackInfo &info) {
114-
if (info.Length() != 1 || !info[0].IsBuffer()) {
115-
throw Napi::Error::New(info.Env(),
116-
"Need one argument: a Buffer of 8-bit counters");
117-
}
118-
119-
auto buf = info[0].As<Napi::Buffer<uint8_t>>();
120-
auto size = buf.Length();
121-
if (size == 0) {
122-
return;
123-
}
124-
125-
RegisterCounterRange(buf.Data(), buf.Data() + size);
126-
}
127-
128117
uint8_t *CoverageCounters() { return gCoverageCounters; }
129118

119+
std::size_t CoverageCountersCapacity() { return gCoverageCountersCapacity; }
120+
130121
std::size_t CoverageCountersSize() { return gCoverageCountersSize; }
131122

123+
std::size_t *CoverageCountersSizePointer() { return &gCoverageCountersSize; }
124+
132125
void ClearCoverageCounters() {
133126
if (gCoverageCounters == nullptr || gCoverageCountersSize == 0) {
134127
return;

packages/fuzzer/shared/coverage.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919

2020
void RegisterCoverageMap(const Napi::CallbackInfo &info);
2121
void RegisterNewCounters(const Napi::CallbackInfo &info);
22-
void RegisterModuleCounters(const Napi::CallbackInfo &info);
2322

2423
uint8_t *CoverageCounters();
24+
std::size_t CoverageCountersCapacity();
2525
std::size_t CoverageCountersSize();
26+
std::size_t *CoverageCountersSizePointer();
2627
void ClearCoverageCounters();

0 commit comments

Comments
 (0)