Skip to content

Fix/eval safety leaks - #23

Merged
otnc merged 3 commits into
mainfrom
fix/eval-safety-leaks
Jul 4, 2026
Merged

Fix/eval safety leaks#23
otnc merged 3 commits into
mainfrom
fix/eval-safety-leaks

Conversation

@otnc

@otnc otnc commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fix bug(s)
  • New feature(s)
  • Others

Why?

Related issue(s)

closes

Check

  • Check if duplicate PR(s) already exist.
  • Run pnpm check (lint, format & typecheck)
  • Run pnpm test (functions test)
  • Run pnpm build (dist/)

otnc added 3 commits July 4, 2026 22:22
…r leaks

Code-review pass over the js/cjs/mjs eval engine and the catchProcessErrors
safety net turned up several real leaks/gaps, all fixed here:

- runVmEval (js/cjs) and jsk mjs's handler submitted a task and installed
  security guards/terminal capture before entering their try block, so a
  throw there (e.g. installSecurityGuards failing) skipped cleanup —
  jsk mjs specifically could leave process.stdout/stderr permanently
  monkeypatched. Moved all of that inside try, with nullable holders so
  catch/finally still see whatever got set up before the throw.
- jsk mjs's globalThis[argsKey] bridge could leak the eval scope (including
  the live client) if writeFileSync failed between setting it and the
  import() that used to be the only place deleting it; now always deleted
  in the outer finally.
- jsk cjs's require('child_process') was a direct, unwrapped path to
  execSync/execFileSync/spawnSync, bypassing the default-timeout guard
  dynamicImport() already gives jsk js/jsk cjs. Added createGuardedRequire
  (shares the same Proxy wrap as createDynamicImport) so require gets it
  too. jsk mjs's static `import` can't be intercepted the same way without
  a process-wide ESM loader hook, so that gap is documented instead.
- catchProcessErrors installed process-wide uncaughtException/
  unhandledRejection listeners with no way to remove them, leaking one
  Jishaku instance's worth of listeners (and everything they close over)
  per construction. Added Jishaku#destroy() to remove them.

jsk mjs's cache-busting import() also permanently grows Node's ESM module
cache by one entry per eval — inherent to needing a fresh module per
invocation with no cache-eviction API, and not fixable without giving up
real top-level import/await support, so it's documented as a known
trade-off rather than "fixed".
- `>=18` to `>=22`
jsk mjs's generated module can reach node:child_process via a real static
import, which — unlike dynamicImport (jsk js) or the guarded require
(jsk cjs) — goes straight through Node's own loader with no per-call
interception point, so it silently bypassed the execSync/execFileSync/
spawnSync default-timeout protection the other two flavors get.

Fixed by installChildProcessTimeoutGuard: temporarily patch the real,
shared child_process module (via a plain require, not the frozen ESM
namespace) right before importing the eval's generated module, then
restore it once the import settles. Confirmed experimentally (Node 22 and
24) that a module loaded while the patch is active observes it through
default, named, and namespace import forms alike, unlike mutating an
already-obtained module object after the fact.

The remaining "not fixed" trade-off for jsk mjs is now only the ESM
module cache growing by one entry per eval (no Node API to evict a
loaded module) — documented in the same doc comment.
@otnc
otnc merged commit 27dbfdd into main Jul 4, 2026
5 checks passed
@otnc
otnc deleted the fix/eval-safety-leaks branch July 4, 2026 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant