From 543a04833a7064beffcc2924a908424487f74fee Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Tue, 10 Feb 2026 16:43:39 -0800 Subject: [PATCH] Condemn the isolate when there is a Python fatal error --- src/pyodide/internal/metadata.ts | 3 +++ src/pyodide/internal/python.ts | 7 +++++++ src/pyodide/types/emscripten.d.ts | 3 +++ .../types/runtime-generated/metadata.d.ts | 1 + src/workerd/api/pyodide/pyodide.c++ | 18 ++++++++++++++++++ src/workerd/api/pyodide/pyodide.h | 5 +++++ src/workerd/io/limit-enforcer.h | 6 ++++++ 7 files changed, 43 insertions(+) diff --git a/src/pyodide/internal/metadata.ts b/src/pyodide/internal/metadata.ts index 0daa3fa76d8..99449767683 100644 --- a/src/pyodide/internal/metadata.ts +++ b/src/pyodide/internal/metadata.ts @@ -70,3 +70,6 @@ export const CHECK_RNG_STATE = !!COMPATIBILITY_FLAGS.python_check_rng_state; export const setCpuLimitNearlyExceededCallback = MetadataReader.setCpuLimitNearlyExceededCallback.bind(MetadataReader); + +export const condemnIsolate: (reason: string) => never = + MetadataReader.condemnIsolate.bind(MetadataReader); diff --git a/src/pyodide/internal/python.ts b/src/pyodide/internal/python.ts index 9aabbe649eb..13e833c4400 100644 --- a/src/pyodide/internal/python.ts +++ b/src/pyodide/internal/python.ts @@ -20,6 +20,7 @@ import { import { LEGACY_VENDOR_PATH, setCpuLimitNearlyExceededCallback, + condemnIsolate, } from 'pyodide-internal:metadata'; /** @@ -241,6 +242,12 @@ export function loadPyodide( ); Module.compileModuleFromReadOnlyFS = compileModuleFromReadOnlyFS; Module.API.config.jsglobals = globalThis; + + // Set up the fatal error handler to condemn the isolate when Pyodide + // encounters an unrecoverable error. + Module.API.on_fatal = (error: any): void => { + condemnIsolate(`${error}`); + }; if (isWorkerd) { Module.API.config.indexURL = indexURL; Module.API.config.resolveLockFilePromise!(lockfile); diff --git a/src/pyodide/types/emscripten.d.ts b/src/pyodide/types/emscripten.d.ts index ef8877cf39b..7a2522ba83b 100644 --- a/src/pyodide/types/emscripten.d.ts +++ b/src/pyodide/types/emscripten.d.ts @@ -40,6 +40,9 @@ interface API { serializeHiwireState(serializer: (obj: any) => any): SnapshotConfig; pyVersionTuple: [number, number, number]; scheduleCallback: (callback: () => void, timeout: number) => void; + // Callback invoked when Pyodide encounters a fatal error. Setting this allows + // the runtime to handle fatal errors (e.g., by condemning the isolate). + on_fatal?: (error: any) => void; } interface LDSO { diff --git a/src/pyodide/types/runtime-generated/metadata.d.ts b/src/pyodide/types/runtime-generated/metadata.d.ts index d4c23239bd1..0abae5c5a73 100644 --- a/src/pyodide/types/runtime-generated/metadata.d.ts +++ b/src/pyodide/types/runtime-generated/metadata.d.ts @@ -36,6 +36,7 @@ declare namespace MetadataReader { sig_clock: number, sig_flag: number ) => void; + const condemnIsolate: (reason: string) => never; const constructor: { getBaselineSnapshotImports(): string[]; }; diff --git a/src/workerd/api/pyodide/pyodide.c++ b/src/workerd/api/pyodide/pyodide.c++ index 3ec0f0871ee..aaf882cc8e6 100644 --- a/src/workerd/api/pyodide/pyodide.c++ +++ b/src/workerd/api/pyodide/pyodide.c++ @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -102,6 +103,23 @@ void PyodideMetadataReader::setCpuLimitNearlyExceededCallback( }); } +[[noreturn]] void PyodideMetadataReader::condemnIsolate(jsg::Lock& js, kj::String reason) { + // Condemn the isolate due to a fatal Pyodide error. This marks the isolate as condemned + // so that future requests will be routed to a new isolate, aborts the current request context, + // and terminates JavaScript execution. + auto description = kj::str("Pyodide fatal error: ", reason); + kj::Exception error(kj::Exception::Type::FAILED, __FILE__, __LINE__, kj::mv(description)); + + // Condemn the isolate so future requests get a fresh one + Worker::Isolate::from(js).getLimitEnforcer().condemn(); + + // Abort the current request context + IoContext::current().abort(kj::cp(error)); + + // Terminate JavaScript execution immediately + js.terminateExecutionNow(); +} + kj::Array PythonModuleInfo::getPythonFileContents() { auto builder = kj::Vector(names.size()); for (auto i: kj::zeroTo(names.size())) { diff --git a/src/workerd/api/pyodide/pyodide.h b/src/workerd/api/pyodide/pyodide.h index 7241b30e9cf..6cbd4797dd5 100644 --- a/src/workerd/api/pyodide/pyodide.h +++ b/src/workerd/api/pyodide/pyodide.h @@ -245,6 +245,10 @@ class PyodideMetadataReader: public jsg::Object { void setCpuLimitNearlyExceededCallback( jsg::Lock& js, kj::Array wasm_memory, int sig_clock, int sig_flag); + // Condemns the isolate by aborting the IoContext and terminating JavaScript execution. + // This should be called when Pyodide encounters a fatal error that cannot be recovered from. + [[noreturn]] void condemnIsolate(jsg::Lock& js, kj::String reason); + // Similar to Cloudflare::::getCompatibilityFlags in global-scope.c++, but the key difference is // that it returns experimental flags even if `experimental` is not enabled. This avoids a gotcha // where an experimental compat flag is enabled in our C++ code, but not in our JS code. @@ -274,6 +278,7 @@ class PyodideMetadataReader: public jsg::Object { JSG_METHOD(getCompatibilityFlags); JSG_STATIC_METHOD(getBaselineSnapshotImports); JSG_METHOD(setCpuLimitNearlyExceededCallback); + JSG_METHOD(condemnIsolate); } void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { diff --git a/src/workerd/io/limit-enforcer.h b/src/workerd/io/limit-enforcer.h index 2f8897b10eb..456f335e310 100644 --- a/src/workerd/io/limit-enforcer.h +++ b/src/workerd/io/limit-enforcer.h @@ -98,6 +98,12 @@ class IsolateLimitEnforcer: public kj::Refcounted { virtual bool hasExcessivelyExceededHeapLimit() const = 0; + // Condemns the isolate due to a fatal error. Requests being handled by condemned isolates + // should be able to complete any in-flight requests (within a grace period) but should not + // accept any new requests. The default implementation is a no-op; subclasses may override + // to implement actual condemnation behavior. + virtual void condemn() const {} + // Inserts a custom mark event named `name` into this isolate's perf event data stream. At // present, this is only implemented internally. Call this function from various APIs to be able // to correlate perf event data with usage of those APIs.