Repository navigation
π Bug Report β Runtime APIs: Legacy module registry recurses without bound (SIGSEGV) when the module fallback service returns a self-redirectΒ #7626
Description
What happened
ModuleRegistryImpl::resolve (src/workerd/jsg/modules.h) caches every redirect the module fallback service returns in fallbackServiceRedirects and immediately re-enters resolve with specifier.parent().eval(redirect). When the redirect target evaluates to the specifier being resolved, the cache entry maps a path to itself. The next call misses the registry, hits the cached-redirect branch, and recurses with the same path until the stack overflows. workerd dies with *** Received signal #11: Segmentation fault, with no KJ exception and no mention of the module.
The common way to hit this is a node:* builtin that is not enabled at the Worker's compat date/flags. workerd roots node:/cloudflare:/workerd: specifiers (/node:os) and asks the fallback for specifier=node:os. A fallback that answers 301 Location: /node:os (as @cloudflare/vitest-pool-workers < 0.21.1 did; see cloudflare/workers-sdk#14590 and #14882) produces the self-redirect. The pool now avoids it, but any fallback service can still crash the runtime this way.
Versions
- workerd 1.20260929.1 (
@cloudflare/workerd-linux-64, release commit 82b85cf) and 1.20260903.1. The code is unchanged onmainat 752ba4d. - Linux x86_64 (Ubuntu 24.04, kernel 7.0.0-28-generic), Node 22.22.0 for the fallback server.
Reproduction (no vitest/miniflare)
config.capnp
using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [ (name = "main", worker = .mainWorker) ],
sockets = [ (name = "http", address = "127.0.0.1:18787", http = (), service = "main") ],
);
const mainWorker :Workerd.Worker = (
modules = [ (name = "main.mjs", esModule = embed "main.mjs") ],
# node:os is a builtin only from 2025-09-15 (or with enable_nodejs_os_module).
compatibilityDate = "2025-06-01",
compatibilityFlags = ["nodejs_compat"],
moduleFallback = "127.0.0.1:18788",
);main.mjs
export default {
async fetch() {
const os = await import('node:os');
return new Response(typeof os.EOL);
},
};fallback.mjs
import http from 'node:http';
http
.createServer((req, res) => {
const url = new URL(req.url, 'http://fallback');
const specifier = url.searchParams.get('specifier');
console.log(`fallback ${req.headers['x-resolve-method']} specifier=${specifier} referrer=${url.searchParams.get('referrer')}`);
if (specifier === 'node:os' || specifier === '/node:os') {
res.writeHead(301, { Location: '/node:os' });
} else {
res.writeHead(404);
}
res.end();
})
.listen(18788, '127.0.0.1');Run:
node fallback.mjs &
workerd serve config.capnp --experimental &
curl http://127.0.0.1:18787/Expected
The import rejects with Error: No such module "node:os" (what workerd reports for the same Worker without moduleFallback), or with an error naming the redirect loop. The process stays up.
Actual
fallback import specifier=node:os referrer=/main.mjs
*** Received signal #11: Segmentation fault
stack: workerd@0x27f0168 workerd@0x27f09fc workerd@0x27f09fc β¦ (25Γ 0x27f09fc)
curl: (52) Empty reply from server
Symbolized with addr2line against the unstripped 1.20260929.1 binary: kj::Path::evalPart β kj::PathPtr::eval β ModuleRegistryImpl<JsgWorkerdIsolate_TypeWrapper>::resolve β resolve β resolve β¦. There is one fallback request, then the crash.
Controls: adding "enable_nodejs_os_module" to compatibilityFlags makes the request return string. Removing moduleFallback gives a clean No such module "node:os".
Suggested fix
Port the two guards from the new registry (jsg/modules-new.c++: the str == normalizedSpecifier alias check and the recursed bound) into ModuleRegistryImpl::resolve, covering both redirect paths (the cached-redirect branch and the fresh-redirect branch). A redirect to the specifier being resolved, or a second redirect hop for the same specifier, should yield kj::none, so the caller raises No such module. Optionally, key fallbackServiceRedirects by method as well: today import and require share one cache entry, although a fallback service may answer them differently.
Activity