From 4f0db97c35dc7b4f69af257e892b0e7d6376f638 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 21 Jan 2026 11:29:41 +0000 Subject: [PATCH 1/2] [miniflare] Fix potential EBADF error when restarting workerd process Explicitly destroy all stdio streams (stdin, stdout, stderr, and control pipe) before killing the workerd process in Runtime#dispose(). This ensures file descriptors are properly released before spawning a new process, preventing EBADF errors during rapid restarts. Fixes #11675 --- .changeset/fix-runtime-ebadf-on-restart.md | 9 +++++ packages/miniflare/src/runtime/index.ts | 24 ++++++++++++- packages/miniflare/test/index.spec.ts | 40 ++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-runtime-ebadf-on-restart.md diff --git a/.changeset/fix-runtime-ebadf-on-restart.md b/.changeset/fix-runtime-ebadf-on-restart.md new file mode 100644 index 000000000000..53027d62bf0d --- /dev/null +++ b/.changeset/fix-runtime-ebadf-on-restart.md @@ -0,0 +1,9 @@ +--- +"miniflare": patch +--- + +Fix potential EBADF error when restarting workerd process + +Previously, when the workerd process was restarted (e.g., via `setOptions()` or Vite server restart), the stdio pipes from the previous process were not explicitly destroyed. This could lead to `EBADF` (Bad File Descriptor) errors during spawn on some systems. + +The `Runtime#dispose()` method now explicitly destroys all stdio streams (stdin, stdout, stderr, and the control pipe) before killing the process to ensure file descriptors are properly released. diff --git a/packages/miniflare/src/runtime/index.ts b/packages/miniflare/src/runtime/index.ts index 2d774275e07b..9fa8791bd74c 100644 --- a/packages/miniflare/src/runtime/index.ts +++ b/packages/miniflare/src/runtime/index.ts @@ -315,13 +315,35 @@ export class Runtime { } dispose(): Awaitable { + const runtimeProcess = this.#process; + if (runtimeProcess === undefined) { + return; + } + + // Clear reference to prevent potential race conditions + this.#process = undefined; + + // Explicitly destroy all stdio streams to ensure file descriptors are + // properly released. This prevents EBADF errors when spawning a new + // process after restart. + // See https://github.com/cloudflare/workers-sdk/issues/11675 + runtimeProcess.stdin?.destroy(); + runtimeProcess.stdout?.destroy(); + runtimeProcess.stderr?.destroy(); + // The control pipe at stdio[3] is a Readable stream + const controlPipe = runtimeProcess.stdio[3]; + if (controlPipe instanceof Readable) { + controlPipe.destroy(); + } + // `kill()` uses `SIGTERM` by default. In `workerd`, this waits for HTTP // connections to close before exiting. Notably, Chrome sometimes keeps // connections open for about 10s, blocking exit. We'd like `dispose()`/ // `setOptions()` to immediately terminate the existing process. // Therefore, use `SIGKILL` which force closes all connections. // See https://github.com/cloudflare/workerd/pull/244. - this.#process?.kill("SIGKILL"); + runtimeProcess.kill("SIGKILL"); + return this.#processExitPromise; } } diff --git a/packages/miniflare/test/index.spec.ts b/packages/miniflare/test/index.spec.ts index 34f73f5bc01d..7843083a3e2c 100644 --- a/packages/miniflare/test/index.spec.ts +++ b/packages/miniflare/test/index.spec.ts @@ -3401,6 +3401,46 @@ test("Miniflare: custom Node outbound service", async () => { ); }); +test("Miniflare: setOptions: can restart workerd multiple times in succession", async () => { + // Regression test for https://github.com/cloudflare/workers-sdk/issues/11675 + // EBADF errors can occur when spawning a new workerd process if the stdio + // pipes from the previous process are not properly cleaned up. This is + // especially relevant when other parts of the system (like worker_threads + // used by Miniflare or other Vite plugins) are also managing file descriptors. + // While this test doesn't reliably reproduce the race condition, it validates + // that the restart mechanism works correctly after the fix. + const mf = new Miniflare({ + port: 0, + modules: true, + script: `export default { + fetch() { + return new Response("version 1"); + } + }`, + }); + useDispose(mf); + + // First request to ensure initial startup is complete + let res = await mf.dispatchFetch("http://localhost"); + expect(await res.text()).toBe("version 1"); + + // Perform multiple rapid setOptions calls to trigger workerd restarts. + // This tests that the stdio pipe cleanup in dispose() works correctly. + for (let i = 2; i <= 5; i++) { + await mf.setOptions({ + port: 0, + modules: true, + script: `export default { + fetch() { + return new Response("version ${i}"); + } + }`, + }); + res = await mf.dispatchFetch("http://localhost"); + expect(await res.text()).toBe(`version ${i}`); + } +}); + test("Miniflare: MINIFLARE_WORKERD_CONFIG_DEBUG controls workerd config file creation", async () => { const originalEnv = process.env.MINIFLARE_WORKERD_CONFIG_DEBUG; const configFilePath = "workerd-config.json"; From d9ed3adf52768dbb2b202f8f00309b5ce2ef1186 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 21 Jan 2026 12:11:20 +0000 Subject: [PATCH 2/2] fix: update comment to be consistent with conditional check --- packages/miniflare/src/runtime/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/miniflare/src/runtime/index.ts b/packages/miniflare/src/runtime/index.ts index 9fa8791bd74c..302378061fae 100644 --- a/packages/miniflare/src/runtime/index.ts +++ b/packages/miniflare/src/runtime/index.ts @@ -330,7 +330,7 @@ export class Runtime { runtimeProcess.stdin?.destroy(); runtimeProcess.stdout?.destroy(); runtimeProcess.stderr?.destroy(); - // The control pipe at stdio[3] is a Readable stream + // The control pipe at stdio[3] could be a Readable stream const controlPipe = runtimeProcess.stdio[3]; if (controlPipe instanceof Readable) { controlPipe.destroy();