Skip to content

fix(js): abandon runtimes that outlive interrupt grace period - #7378

Merged
dwisiswant0 merged 4 commits into
devfrom
bugfix-7376-conc-rw
May 23, 2026
Merged

fix(js): abandon runtimes that outlive interrupt grace period#7378
dwisiswant0 merged 4 commits into
devfrom
bugfix-7376-conc-rw

Conversation

@Mzack9999

@Mzack9999 Mzack9999 commented May 4, 2026

Copy link
Copy Markdown
Member

Proposed changes

Prevent pooled and non-pooled goja runtimes from being reused or cleaned up when
the RunProgram goroutine fails to exit after context cancellation and Interrupt().
In that state, the goroutine may still be mutating runtime state, so touching the
runtime or returning it to the pool can cause fatal concurrent map access panics.

Add an explicit errRuntimeTerminationTimeout path that abandons the runtime,
keeps the related concurrency slot reserved, and releases that slot only from a
reaper after the orphaned goroutine exits. Preserve errors.Is compatibility with
the original context cancellation cause.

Keep normal cleanup behavior unchanged, including callback panic cleanup, and add
regression tests for stuck-interrupt handling that verify runtime abandonment and
delayed slot release.

Fixes #7376

Checklist

  • Pull request is created against the dev branch
  • All checks passed (lint, unit/integration/regression tests etc.) with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling and cleanup of JavaScript runtimes when execution is interrupted, times out, or panics—prevents premature concurrency-slot release, avoids reuse of abandoned runtimes, and reduces resource exhaustion and instability.
  • Tests
    • Added regression tests covering interrupted, stuck, orphaned, and callback-panic scenarios to ensure correct slot release and safe runtime reclamation.

Review Change Stack

@Mzack9999 Mzack9999 self-assigned this May 4, 2026
@Mzack9999 Mzack9999 added the Type: Bug Inconsistencies or issues which will cause an issue or problem for users or implementors. label May 4, 2026
@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Abandons goja runtimes that fail to terminate after an interrupt, delegates orphan cleanup to a reaper, ensures pool slots aren't released until orphans exit, and adds tests verifying abandonment, slot release ordering, and cleanup after callback panic.

Changes

Runtime Pool Race Condition Fix

Layer / File(s) Summary
Error definition and imports
pkg/js/compiler/pool.go
Add errRuntimeTerminationTimeout sentinel and import errors for detection.
executeWithRuntime lifecycle
pkg/js/compiler/pool.go
Refactor to interrupt on ctx.Done(), wait up to 1s for run goroutine, return errRuntimeTerminationTimeout on timeout, support onOrphanExit callback, and extract cleanupRuntime. Reduce result channel buffer to 1.
Pool abandonment handling
pkg/js/compiler/pool.go
Detect errRuntimeTerminationTimeout, set runtimeAbandoned, avoid returning runtime to sync.Pool, skip runtime-state mutations, and log a warning.
Non-pooled slot ownership
pkg/js/compiler/non-pool.go
Conditionally defer ephemeraljsc.Done() guarded by slotOwnedByReaper when errRuntimeTerminationTimeout is observed; add errors import.
Tests and imports
pkg/js/compiler/compiler_test.go
Add TestPooledRuntimeAbandonedReleasesSlotOnlyAfterOrphanExits and TestPooledRuntimeAbandonedWhenInterruptStuck, add TestExecuteWithRuntimeCleansUpAfterCallbackPanic, update TestRequireDoesNotReusePrivilegedModuleCacheAcrossExecutions, and add sync import for coordination.
sequenceDiagram
  participant Caller
  participant executeWithPoolingProgram
  participant executeWithRuntime
  participant Context as ctx.Done
  participant GojaRuntime as goja.Runtime
  participant Pool as sync.Pool
  participant Reaper

  Caller->>executeWithPoolingProgram: ExecuteProgram(program, opts)
  executeWithPoolingProgram->>Pool: Get runtime
  executeWithPoolingProgram->>executeWithRuntime: run program with ctx

  executeWithRuntime->>Context: ctx.Done() (deadline)
  executeWithRuntime->>GojaRuntime: Interrupt()
  executeWithRuntime->>GojaRuntime: wait for run result (up to 1s)

  alt run returns within grace
    executeWithRuntime-->>executeWithPoolingProgram: normal result
    executeWithPoolingProgram->>Pool: Put runtime back
  else run does not return within 1s
    executeWithRuntime-->>executeWithPoolingProgram: errRuntimeTerminationTimeout
    executeWithPoolingProgram->>executeWithPoolingProgram: mark runtimeAbandoned
    executeWithPoolingProgram->>Reaper: spawn onOrphanExit to wait for orphan and release slot
  end

  executeWithPoolingProgram-->>Caller: final result or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

"A rabbit leaps where runtimes hide,
He counts the slots and marks the tide,
When orphaned threads refuse to part,
The pool holds fast — then frees its heart. 🐇✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The pull request implements targeted fixes that directly address the root cause identified in #7376: preventing concurrent access to goja runtimes by deferring cleanup, detecting abandoned runtimes, and ensuring the pool does not reuse runtimes that exceed interrupt grace periods.
Out of Scope Changes check ✅ Passed All changes are focused on JS runtime pool management and concurrency safety; no out-of-scope modifications detected outside the compiler package.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(js): abandon runtimes that outlive interrupt grace period' is directly related to the main change—introducing logic to detect when JS runtimes exceed the interrupt grace period, mark them as abandoned, and prevent their reuse, which is the core of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix-7376-conc-rw

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 and usage tips.

@Mzack9999
Mzack9999 marked this pull request as ready for review May 4, 2026 21:54
@auto-assign
auto-assign Bot requested a review from dwisiswant0 May 4, 2026 21:54
@neo-by-projectdiscovery-dev

neo-by-projectdiscovery-dev Bot commented May 4, 2026

Copy link
Copy Markdown

Neo - PR Security Review

No security issues found

Hardening Notes
  • The cleanupRuntime bypass when runtimeAbandoned=true is intentional and safe - attempting cleanup would race with the orphan goroutine
  • Consider rate limiting or monitoring runtime abandonment events to detect potential DoS attempts via repeated timeout scenarios

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/js/compiler/pool.go`:
- Around line 204-208: The defer currently calls pooljsc.Done() even when
runtimeAbandoned is true, freeing the concurrency slot while the RunProgram
goroutine is still alive (see runtimeAbandoned, errRuntimeTerminationTimeout,
RunProgram, pooljsc.Done(), gojapool.Put); change the logic so that when
runtimeAbandoned==true you do NOT call pooljsc.Done() in this defer but instead
hand the abandoned runtime to a reaper: spawn a goroutine (or enqueue to a
reaper worker) that waits for the orphaned RunProgram to fully exit (or for the
runtime to be Put back via gojapool.Put) and only then calls pooljsc.Done();
apply the same conditional fix to the other defer block referenced (around the
261-264 region).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7f4d1dd9-31fc-4e50-8649-32a1c777f8fe

📥 Commits

Reviewing files that changed from the base of the PR and between a4da890 and 57ec28c.

📒 Files selected for processing (2)
  • pkg/js/compiler/compiler_test.go
  • pkg/js/compiler/pool.go

Comment thread pkg/js/compiler/pool.go
Comment thread pkg/js/compiler/pool.go Outdated
if err := opts.Callback(runtime); err != nil {
// Inner goroutine has not been spawned yet — safe to clean up
// synchronously and return the runtime to the pool.
cleanupRuntime(runtime, args, opts)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm leaning towards deferring this cleanup to preserve UB (like panic) while cleaning runtime state before gojapool reuse. Wdyt?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$ go test -v -run "TestExecuteWithRuntimeCleansUpAfterCallbackPanic" ./pkg/js/compiler/
=== RUN   TestExecuteWithRuntimeCleansUpAfterCallbackPanic
    compiler_test.go:140: 
        	Error Trace:	/home/dw1/Development/PD/nuclei/pkg/js/compiler/compiler_test.go:140
        	Error:      	Should be true
        	Test:       	TestExecuteWithRuntimeCleansUpAfterCallbackPanic
--- FAIL: TestExecuteWithRuntimeCleansUpAfterCallbackPanic (0.00s)
FAIL
FAIL	github.com/projectdiscovery/nuclei/v3/pkg/js/compiler	0.107s
FAIL

Patch: TestExecuteWithRuntimeCleansUpAfterCallbackPanic.patch

When a runtime is abandoned, we must not touch the
goja runtime after `RunProgram` has exceeded the
interrupt grace period. At the same time, cleanup
still needs to run on every normal exit before
returning the runtime to the pool, including setup
callback panics.

Signed-off-by: Dwi Siswanto <git@dw1.io>
@dwisiswant0 dwisiswant0 changed the title Fix race condition fix(js): abandon runtimes that outlive interrupt grace period May 23, 2026
@dwisiswant0
dwisiswant0 merged commit fa73d2e into dev May 23, 2026
15 of 19 checks passed
@dwisiswant0
dwisiswant0 deleted the bugfix-7376-conc-rw branch May 23, 2026 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Bug Inconsistencies or issues which will cause an issue or problem for users or implementors.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] fatal error: concurrent map read and map write

3 participants