emscripten-core / emscripten-core/emscripten
Help interpreting an Emscripten 4.0.9 Wasm imported-call control-flow contradiction
- Dominant language
- C++
- Stars
- 27.6k
- Forks
- 3.6k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 105
Description
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`:
```text
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:
```javascript
API.rawRun("import _pyodide_core")
```
before `_pyodide_core` side-module loading begins.
CPython first obtains `__main__`, and the observed failing path is:
```text
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:
```text
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`:
```text
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:
```text
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:
```text
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:
```text
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:
1. A hand-written Wasm imported-call and terminal ladder behaves correctly.
2. A minimal Emscripten 4.0.9 `MAIN_MODULE=1` probe with imported memory/table, memory growth, Wasm exceptions, and Wasm longjmp behaves correctly.
3. 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
1. Can an `emscripten_asm_const_int` wrapper observe and return JavaScript numeric `1`, while the Wasm caller receives a different effective `i32` value?
2. Can Wasm exception handling, Wasm longjmp, or generated Emscripten control flow redirect execution before an adjacent native `unreachable`?
3. 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?
4. 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?
5. 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?
6. 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.
Contributor guide
Assessment
This issue has not been assessed yet.