Edited to clarify that pycore_interp_init is a locally added diagnostic caller, not part of the official Pyodide 0.29.4 artefact.
Hello,
I’m investigating a defensive compatibility issue involving Pyodide 0.29.4, Emscripten 4.0.9, and AndroidX JavaScriptSandbox.
The goal is to run Pyodide locally and offline inside AndroidX JavaScriptSandbox as a constrained runtime for untrusted scripts. No browser APIs, filesystem, network access, Android bridge, or direct hardware authority are exposed.
I would appreciate guidance on an imported-call control-flow result from a locally rebuilt diagnostic variant of Pyodide.
Important provenance clarification
The official Pyodide 0.29.4 Wasm distributed through jsDelivr contains the three upstream callers of env.emscripten_asm_const_int:
trigger_fatal_error
init_pyodide_proxy
jsproxy_init
The fourth caller described below, in pycore_interp_init, is not present in the official Pyodide artefact.
It was introduced by a small source-level diagnostic patch in our local build. The build otherwise retains the Pyodide 0.29.4, CPython 3.13.2, and Emscripten 4.0.9 lineage.
The original failure also reproduces without this diagnostic caller. The custom caller was added only to investigate the state leading to that failure.
Environment
- Pyodide 0.29.4 source lineage
- CPython 3.13.2
- Emscripten 4.0.9
MAIN_MODULE=1
- imported memory and table
- memory growth enabled
- Wasm exceptions
SUPPORT_LONGJMP=wasm
- modularised Emscripten runtime
- Android WebView and AndroidX JavaScriptSandbox comparison hosts
Original upstream-baseline failure
The original Pyodide failure occurs during the first:
API.rawRun("import _pyodide_core")
before _pyodide_core side-module loading begins.
CPython first obtains __main__, and the observed failing path is:
PyImport_AddModuleRef("__main__")
→ PyMapping_GetOptionalItem()
→ PyDict_CheckExact()
→ WebAssembly memory access out of bounds
The object-header slot derived from interp->imports.modules is outside the current Wasm linear-memory range. This reproduces on two physical Android devices.
Local diagnostic caller
To investigate an earlier point in the failing path, we added one source-level diagnostic call during interpreter import-state initialisation.
The diagnostic callback returns only:
1 → take a native unreachable path
2 → perform a native byte load at exactly memory.size
Any other return preserves compiler-visible fallthrough.
Static WABT inspection of the locally rebuilt Wasm finds four callers of emscripten_asm_const_int:
trigger_fatal_error
init_pyodide_proxy
jsproxy_init
pycore_interp_init // locally added diagnostic caller
The three upstream callers pass an empty signature. Only the locally added pycore_interp_init caller passes a single-integer signature.
Its verified native control flow is:
imported callback returns 1
→ native unreachable
imported callback returns 2
→ native load at exactly memory.size
otherwise
→ fallthrough
Dynamic observation
We wrapped the actual imported emscripten_asm_const_int function in trusted host code.
The wrapper:
- calls the original import exactly once;
- preserves its arguments and
this binding;
- returns the original value unchanged;
- rethrows any exception unchanged;
- emits only bounded categorical evidence.
On Android WebView, using the same locally rebuilt JavaScript/Wasm pair, we observe:
caller signature: single-i
original imported return: 1
runtime initialization later completes
main later completes
finalizeBootstrap later starts
Static caller attribution establishes that the single-integer signature uniquely identifies the locally added pycore_interp_init caller.
The callback event and subsequent lifecycle events have also been bound to the same recorded runtime generation and instance lineage.
Our present interpretation is therefore:
local pycore_interp_init diagnostic calls emscripten_asm_const_int
→ JavaScript callback returns 1
→ native Wasm should execute unreachable
→ later lifecycle execution nevertheless appears to continue
We are trying to determine which assumption in that interpretation is wrong.
Reduction results
Smaller probes do not reproduce the anomaly:
- A hand-written Wasm imported-call and terminal ladder behaves correctly.
- A minimal Emscripten 4.0.9
MAIN_MODULE=1 probe with imported memory/table, memory growth, Wasm exceptions, and Wasm longjmp behaves correctly.
- A minimal
EM_ASM_INT probe using the same return/unreachable/boundary-load pattern behaves correctly in AndroidX JavaScriptSandbox.
This suggests the behaviour requires an additional condition present in the full Pyodide-derived runtime.
Questions
-
Can an emscripten_asm_const_int wrapper observe and return JavaScript numeric 1, while the Wasm caller receives a different effective i32 value?
-
Can Wasm exception handling, Wasm longjmp, or generated Emscripten control flow redirect execution before an adjacent native unreachable?
-
Is there any MAIN_MODULE, relocation, exception-handling, or runtime-generation mechanism that could make the statically inspected caller differ from the effective executing path despite matched Wasm identity and recorded instance lineage?
-
Is there a known Emscripten 4.0.9 issue involving EM_ASM_INT, MAIN_MODULE, Wasm exceptions, or SUPPORT_LONGJMP=wasm that could be relevant?
-
What source-level instrumentation would you recommend to prove the value consumed by the Wasm caller without modifying generated JavaScript or editing the Wasm binary?
-
Is there a later Emscripten version that would provide a particularly meaningful controlled comparison?
We can provide:
- the exact small source patch that adds the diagnostic caller;
- the complete compile flags;
- the minimal A1/A2/A3 probe sources;
- the static four-call-site attribution table;
- the locally generated JavaScript/Wasm pair;
- bounded comparison results from Node, Android WebView, AndroidX JavaScriptSandbox, physical devices, and an API 35 emulator;
- a minimal Android host project if useful.
We are not trying to suppress the original memory trap or patch around corrupted CPython state. The goal is to understand the source-level mechanism and determine whether a narrow, upstream-compatible correction is possible.
Thank you for any guidance.
Edited to clarify that pycore_interp_init is a locally added diagnostic caller, not part of the official Pyodide 0.29.4 artefact.
Hello,
I’m investigating a defensive compatibility issue involving Pyodide 0.29.4, Emscripten 4.0.9, and AndroidX JavaScriptSandbox.
The goal is to run Pyodide locally and offline inside AndroidX JavaScriptSandbox as a constrained runtime for untrusted scripts. No browser APIs, filesystem, network access, Android bridge, or direct hardware authority are exposed.
I would appreciate guidance on an imported-call control-flow result from a locally rebuilt diagnostic variant of Pyodide.
Important provenance clarification
The official Pyodide 0.29.4 Wasm distributed through jsDelivr contains the three upstream callers of
env.emscripten_asm_const_int:The fourth caller described below, in
pycore_interp_init, is not present in the official Pyodide artefact.It was introduced by a small source-level diagnostic patch in our local build. The build otherwise retains the Pyodide 0.29.4, CPython 3.13.2, and Emscripten 4.0.9 lineage.
The original failure also reproduces without this diagnostic caller. The custom caller was added only to investigate the state leading to that failure.
Environment
MAIN_MODULE=1SUPPORT_LONGJMP=wasmOriginal upstream-baseline failure
The original Pyodide failure occurs during the first:
before
_pyodide_coreside-module loading begins.CPython first obtains
__main__, and the observed failing path is:The object-header slot derived from
interp->imports.modulesis outside the current Wasm linear-memory range. This reproduces on two physical Android devices.Local diagnostic caller
To investigate an earlier point in the failing path, we added one source-level diagnostic call during interpreter import-state initialisation.
The diagnostic callback returns only:
Any other return preserves compiler-visible fallthrough.
Static WABT inspection of the locally rebuilt Wasm finds four callers of
emscripten_asm_const_int:The three upstream callers pass an empty signature. Only the locally added
pycore_interp_initcaller passes a single-integer signature.Its verified native control flow is:
Dynamic observation
We wrapped the actual imported
emscripten_asm_const_intfunction in trusted host code.The wrapper:
thisbinding;On Android WebView, using the same locally rebuilt JavaScript/Wasm pair, we observe:
Static caller attribution establishes that the single-integer signature uniquely identifies the locally added
pycore_interp_initcaller.The callback event and subsequent lifecycle events have also been bound to the same recorded runtime generation and instance lineage.
Our present interpretation is therefore:
We are trying to determine which assumption in that interpretation is wrong.
Reduction results
Smaller probes do not reproduce the anomaly:
MAIN_MODULE=1probe with imported memory/table, memory growth, Wasm exceptions, and Wasm longjmp behaves correctly.EM_ASM_INTprobe using the same return/unreachable/boundary-load pattern behaves correctly in AndroidX JavaScriptSandbox.This suggests the behaviour requires an additional condition present in the full Pyodide-derived runtime.
Questions
Can an
emscripten_asm_const_intwrapper observe and return JavaScript numeric1, while the Wasm caller receives a different effectivei32value?Can Wasm exception handling, Wasm longjmp, or generated Emscripten control flow redirect execution before an adjacent native
unreachable?Is there any
MAIN_MODULE, relocation, exception-handling, or runtime-generation mechanism that could make the statically inspected caller differ from the effective executing path despite matched Wasm identity and recorded instance lineage?Is there a known Emscripten 4.0.9 issue involving
EM_ASM_INT,MAIN_MODULE, Wasm exceptions, orSUPPORT_LONGJMP=wasmthat could be relevant?What source-level instrumentation would you recommend to prove the value consumed by the Wasm caller without modifying generated JavaScript or editing the Wasm binary?
Is there a later Emscripten version that would provide a particularly meaningful controlled comparison?
We can provide:
We are not trying to suppress the original memory trap or patch around corrupted CPython state. The goal is to understand the source-level mechanism and determine whether a narrow, upstream-compatible correction is possible.
Thank you for any guidance.