Skip to content

Commit 5c102ed

Browse files
committed
fix(fuzzer): defer LibAFL promise settlement
Async findings used to settle before native cleanup released the single-run runtime guard. Defer rejection until finalization so CLI exit and follow-up runs cannot race teardown.
1 parent 96909a7 commit 5c102ed

2 files changed

Lines changed: 58 additions & 12 deletions

File tree

packages/fuzzer/libafl_runtime.cpp

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ struct AsyncFuzzTargetContext {
169169

170170
std::thread native_thread;
171171
Napi::Promise::Deferred deferred;
172+
Napi::Reference<Napi::Value> deferred_rejection;
172173
LibAflOptions options;
173174
std::unique_ptr<ScopedLibAflRuntime> runtime_guard;
174175
bool is_resolved = false;
@@ -190,13 +191,12 @@ AsyncFuzzTargetContext *gActiveAsyncContext = nullptr;
190191
AsyncTsfn gAsyncTsfn;
191192
JazzerLibAflFindingInfo gFindingInfo{};
192193

193-
void RejectDeferredIfNeeded(AsyncFuzzTargetContext *context,
194+
void StoreDeferredRejection(AsyncFuzzTargetContext *context,
194195
const Napi::Value &error) {
195-
if (context->is_resolved) {
196+
if (!context->deferred_rejection.IsEmpty()) {
196197
return;
197198
}
198-
context->deferred.Reject(error);
199-
context->is_resolved = true;
199+
context->deferred_rejection = Napi::Persistent(error);
200200
}
201201

202202
void SettleLibAflRun(Napi::Env env, Napi::Promise::Deferred &deferred,
@@ -243,13 +243,29 @@ void ReportAsyncFinding(AsyncFuzzTargetContext *context, Napi::Env env,
243243
const std::shared_ptr<AsyncExecutionState> &state,
244244
const Napi::Value &error,
245245
const std::vector<uint8_t> &input) {
246+
StoreDeferredRejection(context, error);
246247
if (TrySetExecutionStatus(state, kExecutionFinding)) {
247248
const auto artifact =
248249
WriteArtifact(context->options.artifact_prefix, "crash", input.data(),
249250
input.size(), false);
250251
RecordFindingInfo(&gFindingInfo, artifact, DescribeJsError(env, error));
251252
}
252-
RejectDeferredIfNeeded(context, error);
253+
}
254+
255+
void SettleAsyncLibAflRun(Napi::Env env, AsyncFuzzTargetContext *context) {
256+
if (context->is_resolved) {
257+
return;
258+
}
259+
260+
if (!context->deferred_rejection.IsEmpty()) {
261+
context->is_resolved = true;
262+
context->deferred.Reject(context->deferred_rejection.Value());
263+
context->deferred_rejection.Reset();
264+
return;
265+
}
266+
267+
SettleLibAflRun(env, context->deferred, context->is_resolved,
268+
context->run_status);
253269
}
254270

255271
void StartSyncWatchdog(SyncFuzzTargetContext *context) {
@@ -424,8 +440,6 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback,
424440
try {
425441
if (context->sigints > 0) {
426442
TrySetExecutionStatus(state, kExecutionStop);
427-
context->deferred.Resolve(env.Undefined());
428-
context->is_resolved = true;
429443
return;
430444
}
431445

@@ -522,10 +536,11 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function js_fuzz_callback,
522536
} catch (const Napi::Error &error) {
523537
ReportAsyncFinding(context, env, state, error.Value(), current_input);
524538
} catch (const std::exception &exception) {
525-
TrySetExecutionStatus(state, kExecutionFatal);
526-
auto message =
527-
std::string("Internal fuzzer error - ").append(exception.what());
528-
RejectDeferredIfNeeded(context, Napi::Error::New(env, message).Value());
539+
if (TrySetExecutionStatus(state, kExecutionFatal)) {
540+
auto message =
541+
std::string("Internal fuzzer error - ").append(exception.what());
542+
StoreDeferredRejection(context, Napi::Error::New(env, message).Value());
543+
}
529544
}
530545
}
531546

@@ -673,8 +688,10 @@ Napi::Value StartLibAflAsync(const Napi::CallbackInfo &info) {
673688
info.Env(), info[0].As<Napi::Function>(), "LibAflAsyncAddon", 0, 1,
674689
context.get(),
675690
[](Napi::Env env, AsyncFinalizerDataType *, AsyncFuzzTargetContext *ctx) {
691+
Napi::HandleScope scope(env);
676692
ctx->native_thread.join();
677-
SettleLibAflRun(env, ctx->deferred, ctx->is_resolved, ctx->run_status);
693+
ctx->runtime_guard.reset();
694+
SettleAsyncLibAflRun(env, ctx);
678695
delete ctx;
679696
});
680697

packages/fuzzer/libafl_runtime.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616

1717
import { spawnSync } from "child_process";
18+
import * as fs from "fs";
19+
import * as os from "os";
1820
import * as path from "path";
1921

2022
import { addon } from "./addon";
@@ -98,6 +100,33 @@ describe("LibAFL runtime", () => {
98100
}
99101
});
100102

103+
it("settles async findings after releasing the native runtime", async () => {
104+
const artifactDirectory = fs.mkdtempSync(
105+
path.join(os.tmpdir(), "jazzer-libafl-lifetime-"),
106+
);
107+
108+
try {
109+
await expect(
110+
addon.startLibAflAsync(
111+
() => {
112+
throw new Error("first finding");
113+
},
114+
{
115+
...libAflOptions,
116+
artifactPrefix: `${artifactDirectory}${path.sep}`,
117+
},
118+
),
119+
).rejects.toThrow("first finding");
120+
121+
await addon.startLibAflAsync(() => undefined, {
122+
...libAflOptions,
123+
runs: 1,
124+
});
125+
} finally {
126+
fs.rmSync(artifactDirectory, { force: true, recursive: true });
127+
}
128+
});
129+
101130
it("restores previous SIGINT handlers after fuzzing", () => {
102131
const addonPath = path.join(
103132
__dirname,

0 commit comments

Comments
 (0)