Skip to content

fix(core): honour the scan context when acquiring worker slots and JS runtimes - #7678

Open
gnuletik wants to merge 1 commit into
projectdiscovery:devfrom
gnuletik:fix/cancellable-template-and-js-waits
Open

fix(core): honour the scan context when acquiring worker slots and JS runtimes#7678
gnuletik wants to merge 1 commit into
projectdiscovery:devfrom
gnuletik:fix/cancellable-template-and-js-waits

Conversation

@gnuletik

@gnuletik gnuletik commented Aug 26, 2026

Copy link
Copy Markdown

Problem

Four acquires in the scan path ignore the context they are given, so a cancelled scan cannot leave until unrelated work finishes.

location acquire
pkg/core/execute_options.go executeTemplateSpray wg.Add()AdaptiveWaitGroup.AddWithContext(context.Background())
pkg/core/execute_options.go executeHostSpray wp.Add() → same
pkg/core/executors.go executeTemplatesOnTarget sg.Add() → same
pkg/tmplexec/flow/vm.go GetJSRuntime sizedgojapool.Get(context.TODO())

None of these return quickly under load. A worker slot is held for as long as a template runs, and a JS runtime stays checked out for the whole flow — including any wait on the rate limiter, since flow acquires the runtime in ExecuteWithResults and only then reaches requestExecutorrateLimitTake.

Both spray loops already check ctx.Done() at the top of each iteration, so the acquire is the only place cancellation is invisible. Once a goroutine is inside it, the check above can never be reached again.

Why it matters

flow's goja pool is process-global — built once under jsOnce, shared by every scan in the process, and clamped to a minimum of 100. Running the engine as a library with several concurrent scans, the pool saturates, and a scan whose context is already cancelled queues for a runtime it only needs in order to unwind. It cannot leave until an unrelated scan releases one.

We hit this in production. Goroutine profiles from seven wedged scans, across six processes and four hosts:

profile runtimes checked out goroutines queued in GetJSRuntime
1 100 106
2 100 69
3 100 62
4 100 255
5 100 52
6 100 89
7 100 119

Every one sits exactly on the pool cap. In each, the scan's own goroutine is parked in:

semaphore.(*Weighted).Acquire
  <- syncutil.(*AdaptiveWaitGroup).AddWithContext
  <- core.(*Engine).executeTemplateSpray

waiting for a worker slot held by goroutines that are themselves waiting on sizedpool.Get(context.TODO()). Cancelling the scan changes nothing, because neither wait observes it.

Change

  • AdaptiveWaitGroup.Add()AddWithContext(ctx) at the three worker-pool sites, returning when it errors. AddWithContext decrements its own counter and skips wg.Add(1) on failure, so the error paths correctly do not call Done().
  • GetJSRuntime takes a context.Context and returns an error, passing it to sizedpool.Get.

pkg/js/compiler already does this correctly via pooljsc.AddWithContext(ctx); only flow did not.

Notes for review

Exported signature change. GetJSRuntime(opts) becomes GetJSRuntime(ctx, opts) (*goja.Runtime, error). It has one in-tree caller. If you would rather not break it, happy to add GetJSRuntimeWithContext and leave a deprecated wrapper — say the word.

workflow_execute.go deliberately untouched. It has the same swg.Add() pattern in two places, but workflow execution is a separate path and I have no evidence of it wedging, so I left it out rather than widen the diff on speculation. Glad to include it if you want the pattern fixed consistently.

Test

TestGetJSRuntimeHonoursContext covers both directions: a live context still gets a runtime, a cancelled one gets an error. semaphore.Acquire reports a cancelled context before it checks whether a slot is free, so the test does not need to saturate the pool to be meaningful. Reverting Get(ctx) to Get(context.TODO()) fails it immediately with An error is expected but got nil.

go build ./..., go vet, and go test ./pkg/core/... ./pkg/tmplexec/... pass locally.

Summary by CodeRabbit

  • Bug Fixes
    • Canceled scans now stop promptly when waiting for execution capacity.
    • Prevented new template executions from starting after cancellation.
    • JavaScript runtime acquisition now respects scan cancellation and reports acquisition errors.
  • Tests
    • Added coverage confirming canceled scans do not remain blocked while waiting for a JavaScript runtime.
… runtimes

Four acquires in the scan path ignored the context they were given, so a
cancelled scan could not leave until unrelated work finished:

- executeTemplateSpray and executeTemplatesOnTarget acquire a worker slot via
  AdaptiveWaitGroup.Add, which passes context.Background().
- executeHostSpray does the same.
- flow.GetJSRuntime acquires from a process-global goja pool with
  sizedpool.Get(context.TODO()).

Each of these can wait a long time. A worker slot is held for as long as a
template runs, and a JS runtime stays checked out for the whole flow including
any wait on the rate limiter, so the pool saturates under concurrent scans.
A scan whose context is already cancelled then queues for a runtime it only
needs in order to unwind, and its own cancellation cannot break the wait.

Both spray loops already check ctx.Done() at the top of each iteration, so the
acquire was the only place cancellation was invisible.

GetJSRuntime now takes a context and returns an error. This changes an
exported signature; it has one caller in-tree.
@neo-by-projectdiscovery-dev

neo-by-projectdiscovery-dev Bot commented Aug 26, 2026

Copy link
Copy Markdown

Neo - PR Security Review

No exploitable security vulnerabilities introduced by this PR — all changes are context propagation for cancellation signaling with no attacker-controlled data flowing through the affected paths.

What Neo reviewed

pkg/core/execute_options.go, pkg/core/executors.go, pkg/tmplexec/flow/vm.go, pkg/tmplexec/flow/flow_executor.go

Comment @pdneo help for available commands. · Open in Neo

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fa539c85-c6e6-4f25-b2cb-566b24ba914a

📥 Commits

Reviewing files that changed from the base of the PR and between 84b464e and 841cd85.

📒 Files selected for processing (5)
  • pkg/core/execute_options.go
  • pkg/core/executors.go
  • pkg/tmplexec/flow/flow_executor.go
  • pkg/tmplexec/flow/vm.go
  • pkg/tmplexec/flow/vm_test.go

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


Walkthrough

Execution scheduling and JavaScript runtime acquisition now honor cancellation contexts. Canceled scans stop waiting for workpool or runtime capacity, and runtime acquisition errors propagate through flow execution.

Changes

Execution cancellation

Layer / File(s) Summary
Context-aware workpool acquisition
pkg/core/execute_options.go, pkg/core/executors.go
Template-spray, host-spray, and target execution use AddWithContext(ctx). Canceled contexts stop further scheduling and return early.
Context-aware JavaScript runtime flow
pkg/tmplexec/flow/vm.go, pkg/tmplexec/flow/flow_executor.go, pkg/tmplexec/flow/vm_test.go
GetJSRuntime accepts a context and returns acquisition errors. Flow execution handles those errors, and tests verify canceled-context behavior.

Estimated code review effort: 2 (Simple) | ~15 minutes

Merge Risk: ⚪ Minimal · up to 841cd

This change makes scan and runtime waits honor cancellation, allowing cancelled scans to exit instead of waiting on unrelated work. No actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: mzack9999

Poem

A rabbit waits where work slots gleam,
Then hops away from a canceled stream.
The runtime listens, swift and bright,
And leaves the pool without a fight.
Scan paths pause when context says,
“No more hops through busy maze.”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: scan contexts now control worker-slot and JavaScript-runtime acquisition.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 5 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant