Skip to content

fix(js): sandbox esbuild's ResolveDir instead of the real filesystem root - #65

Merged
tannevaled merged 1 commit into
mainfrom
fix/esbuild-glob-import-sandbox-resolvedir
Aug 31, 2026
Merged

tannevaled merged 1 commit into
mainfrom
fix/esbuild-glob-import-sandbox-resolvedir

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Summary

  • Security + perf: every OnLoadResult (and the Stdin entry) esbuild's module bundler uses declared ResolveDir: "/". This fully intercepts ordinary import resolution via our plugin, but esbuild's bundler special-cases a glob dynamic import (import(\./${lang}/index.js`), ordinary bundler-emitted locale/route code-splitting) by resolving it directly against ResolveDiron the **real filesystem**, entirely bypassingOnResolve/OnLoad`.
  • With ResolveDir: "/", a page merely containing such an import made the engine recursively walk the host's real root filesystem (readdir + symlink resolution) looking for "matches" — this is a sandbox leak, not just a slowdown: a remote page should never cause this engine to read the host's real directory tree.
  • Fix: esbuildSandboxResolveDir() lazily creates one directory (os.MkdirTemp) that stays empty for the process lifetime; every ResolveDir now points there, so a glob import resolves to zero matches immediately instead.

Impact

  • Rendering a production MDN page (developer.mozilla.org/en-US/docs/Web/CSS) dropped from ~14.7s to ~0.56s (26x) — found via pprof, which showed 88-92% of total CPU inside esbuild/internal/fs readdir/symlink syscalls via resolver.ResolveGlob.
  • github.com/golang/go (also ships module scripts) dropped from ~3.3s to ~1.6s.
  • Pages without module scripts are unaffected.

Test plan

  • go test -short ./... — full suite green.
  • New TestEsbuildSandboxResolveDirIsIsolated: the sandbox directory exists, is empty, and is memoized across calls.
  • New TestModuleGlobImportDoesNotWalkRealFilesystem: regression guard — verified it actually fails (hits the test's 5s bound walking the real filesystem) when the fix is reverted locally, and passes in ~30ms with the fix.
  • Verified live against https://developer.mozilla.org/en-US/docs/Web/CSS with cmd/render before/after (14.7s → 0.56s).

…root

Rendering a production MDN page took ~14.7s wall-clock; pprof showed 88-92%
of total CPU inside esbuild's internal/fs (readdir, symlink resolution) via
resolver.ResolveGlob, called from bundler.parseFile.

Root cause: every OnLoadResult returned by the webengine-http plugin (and
the Stdin entry) declared ResolveDir "/". The plugin fully intercepts
ordinary import resolution, but esbuild's bundler special-cases a *glob*
import — `import(`./${lang}/index.js`)`, ordinary output from a bundler's
locale/route code-splitting, not anything unusual — by resolving it
directly against ResolveDir on the real filesystem via its own internal
resolver, entirely bypassing OnResolve/OnLoad. With ResolveDir "/" and a
pattern whose wildcard has no fixed directory ahead of it (so the walk
starts at ResolveDir itself, not a named subdirectory), a page merely
containing such an import made the engine recursively walk the host's
entire real root filesystem, following symlinks, to find "matches" — for
every module chunk that happened to contain one. Independent of the cost,
the host's real directory tree is not something a remote page should ever
cause this engine to read: this is a sandbox leak, not just a slowdown.

Fix: esbuildSandboxResolveDir() lazily creates one directory (os.MkdirTemp)
that stays empty for the life of the process, and every ResolveDir esbuild
sees now points there instead of "/". A glob import now resolves to zero
matches immediately — a browser's own sandboxed module loader would do the
same — rather than touching the real filesystem at all.

Rendering the same MDN page dropped from ~14.7s to ~0.56s (26x); github.com
(which also ships module scripts) dropped from ~3.3s to ~1.6s. Pages
without module scripts are unaffected.

Two new tests: TestEsbuildSandboxResolveDirIsIsolated asserts the directory
exists, is empty, and is memoized; TestModuleGlobImportDoesNotWalkRealFile-
system is the regression guard — reverting the fix locally reproduces it
failing (it hits the test's 5s bound walking the real filesystem instead
of completing in milliseconds against the sandbox).
@tannevaled
tannevaled merged commit bb59570 into main Aug 31, 2026
7 checks passed
@tannevaled
tannevaled deleted the fix/esbuild-glob-import-sandbox-resolvedir branch August 31, 2026 17:53
tannevaled added a commit that referenced this pull request Aug 31, 2026
Regenerates results.json/REPORT.md's table and montages against the merged
grid track-sizing fix (#63), the esbuild sandbox-escape fix (#65), and the
two perf dedupe wins (#64, #66). Records all four in FIDELITY.md's dated
log, closes the CSS Grid "Known gaps" bullet (now fixed), and adds an honest
note on tailwindcss.com's SSIM number after visually checking the montage.
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