Skip to content

Commit 6af3a28

Browse files
committed
fix(core): avoid double-settle in async finding handling
When a before-hook throws inside an async function body, the async function returns a rejected Promise AND the finding is stored synchronously. Previously, both paths would resolve the C++ promise and deferred (via the synchronous throw's catch block and the .then() rejection handler's microtask), which is undefined behavior that can hang forked child processes. Check clearFirstFinding() before attaching .then() handlers. If a synchronous finding exists, suppress the rejected Promise and handle the finding exclusively through the synchronous path.
1 parent c718e7a commit 6af3a28

1 file changed

Lines changed: 31 additions & 13 deletions

File tree

packages/core/core.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -369,20 +369,38 @@ export function asFindingAwareFuzzFn(
369369
try {
370370
callbacks.runBeforeEachCallbacks();
371371
result = (originalFuzzFn as fuzzer.FuzzTargetAsyncOrValue)(data);
372-
// Explicitly set promise handlers to process findings, but still return
373-
// the fuzz target result directly, so that sync execution is still
374-
// possible.
375372
if (isPromiseLike(result)) {
376-
result = result.then(
377-
(result) => {
378-
callbacks.runAfterEachCallbacks();
379-
return throwIfError() ?? result;
380-
},
381-
(reason) => {
382-
callbacks.runAfterEachCallbacks();
383-
return throwIfError(reason);
384-
},
385-
);
373+
// Check if a finding was already detected synchronously
374+
// (e.g., a before-hook threw inside an async function body,
375+
// which stores the finding and returns a rejected Promise).
376+
// If so, handle it synchronously and do NOT attach .then()
377+
// handlers, as that would cause BOTH the synchronous throw
378+
// (caught by the C++ catch block) AND the .then() rejection
379+
// handler to resolve the C++ promise and deferred -- which
380+
// is undefined behavior (double set_value on std::promise,
381+
// double napi_reject_deferred) that can hang forked child
382+
// processes.
383+
const syncFinding = clearFirstFinding();
384+
if (syncFinding) {
385+
// Suppress the unhandled rejection from the abandoned
386+
// rejected Promise returned by the async fuzz target.
387+
result.catch(() => {});
388+
callbacks.runAfterEachCallbacks();
389+
fuzzTargetError = syncFinding;
390+
} else {
391+
// No synchronous finding -- let the async chain handle
392+
// findings that occur during promise resolution.
393+
result = result.then(
394+
(result) => {
395+
callbacks.runAfterEachCallbacks();
396+
return throwIfError() ?? result;
397+
},
398+
(reason) => {
399+
callbacks.runAfterEachCallbacks();
400+
return throwIfError(reason);
401+
},
402+
);
403+
}
386404
} else {
387405
callbacks.runAfterEachCallbacks();
388406
}

0 commit comments

Comments
 (0)