From 9eceacea17e207370f2cbda00741332ee8524187 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Mon, 20 Jul 2026 08:03:59 -0400 Subject: [PATCH] fix(wasm): boot real firmware past allocator and signature crashes Drop EMULATE_FUNCTION_POINTER_CASTS (it broke the wasm32 TCG ffi_call_js path with BigInt errors) and fix the underlying function-pointer signature mismatches instead: object_class_cmp GCompareFunc, esp_sha hash-callback wrappers, and an xts_aes NULL guard in the ESP32-C3 cache. Backport the upstream Emscripten anonymous-RAM allocator fix. Firmware now boots and initializes the X3 panel; a post-init boot stall remains. --- HANDOFF.md | 13 ++- README.md | 9 +- ROADMAP.md | 5 +- qemu/patches/0002-qemu-wasm.patch | 156 ++++++++++++++++++++++++++++++ scripts/build-qemu-wasm.sh | 6 +- 5 files changed, 178 insertions(+), 11 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index f9406d7..2f9f5c3 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -265,9 +265,16 @@ ADC ladders on GPIO1 & GPIO2; power button GPIO3; UC8253 @ 16 MHz. `-sEMULATE_FUNCTION_POINTER_CASTS=1` is required for QEMU's QOM callbacks. - The WASM dependency image omits libgcrypt. AES/RSA/DS/XTS devices are therefore not instantiated; their MMIO falls through to the existing zero-return stub. -- Real firmware currently stops in `ffi_call_js`: Emscripten's - `EMULATE_FUNCTION_POINTER_CASTS` conflicts with libffi/BigInt. The paused - machine and browser bridge smoke tests pass; full browser boot does not yet. +- Real firmware now boots past the earlier crashes and initializes the X3 panel. + It stalls before painting a full home screen (display generation freezes at 2 + after early init) — the next investigation, likely timer/interrupt or slow TCG. +- Function-pointer signature fixes (required after dropping + `EMULATE_FUNCTION_POINTER_CASTS`, which broke the wasm32 TCG `ffi_call_js` + path): `object_class_cmp` → `GCompareFunc`, `esp_sha` hash-callback wrappers, + and an `xts_aes != NULL` guard in the ESP32-C3 cache (XTS is gcrypt-gated and + absent in WASM; unencrypted flash is unaffected). +- Emscripten pthread startup has a rare transient trap at proxied entry + (`invokeEntryPoint` → `dynCall_ii`); the build retries the bridge smoke once. - The upstream Emscripten RAM allocator fix is backported: anonymous guest RAM uses `qemu_memalign` instead of the broken partial-unmap `mmap` workaround. - Emscripten pthreads require cross-origin isolation (COOP/COEP) in the browser. diff --git a/README.md b/README.md index b4ea0a5..6584046 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,11 @@ map to the device's ADC button ladders and active-low Power GPIO. `make web` serves the required COOP/COEP headers for Emscripten pthreads; a generic static server without those headers will not work. -Real firmware execution currently stops at an Emscripten libffi/function-pointer -signature error. The UI and paused-machine bridge smoke tests work, but browser -boot is not complete. X4 remains a white display stub, and browser SD storage is -not implemented yet. +Real firmware now boots in WASM: it runs past the earlier allocator and +function-pointer signature crashes and initializes the X3 panel (528×792). It +still stalls before painting a full home screen (the display generation counter +freezes after early init), so end-to-end boot is not complete. X4 remains a +white display stub, and browser SD storage is not implemented yet. ## Contents diff --git a/ROADMAP.md b/ROADMAP.md index 2523bb6..5fa15d3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -38,8 +38,9 @@ X4 SPI traffic confirmed the firmware selected its SSD1677 driver. `make qemu-wasm` produces the Emscripten module, WASM binary, and pthread worker under `dist/wasm/`; Node smoke checks confirm `xteink` is registered and its browser framebuffer/input bridge initializes. `make web` serves a dependency-free -UI for firmware, canvas, and buttons. Real firmware execution remains blocked by -an Emscripten libffi/function-pointer signature error. +UI for firmware, canvas, and buttons. Real firmware now boots past the allocator +and function-pointer crashes and initializes the X3 panel, but still stalls +before painting a full home screen. ### Device selection (X3 vs X4) diff --git a/qemu/patches/0002-qemu-wasm.patch b/qemu/patches/0002-qemu-wasm.patch index dba4823..0a3812b 100644 --- a/qemu/patches/0002-qemu-wasm.patch +++ b/qemu/patches/0002-qemu-wasm.patch @@ -450,6 +450,139 @@ index 9ec8bd9244..754990968c 100644 default: return 0; } +diff --git a/hw/misc/esp32c3_cache.c b/hw/misc/esp32c3_cache.c +index 1879b5c2df..e3f09cc51b 100644 +--- a/hw/misc/esp32c3_cache.c ++++ b/hw/misc/esp32c3_cache.c +@@ -58,7 +58,6 @@ static inline uint32_t esp32c3_read_mmu_value(ESP32C3CacheState *s, hwaddr reg_a + + static inline void esp32c3_write_mmu_value(ESP32C3CacheState *s, hwaddr reg_addr, uint32_t value) + { +- ESP32C3XtsAesClass *xts_aes_class = ESP32C3_XTS_AES_GET_CLASS(s->xts_aes); + /* Make the assumption that the address is aligned on sizeof(uint32_t) */ + const uint32_t index = reg_addr / sizeof(uint32_t); + /* Reserved bits shall always be 0 */ +@@ -82,8 +81,12 @@ static inline void esp32c3_write_mmu_value(ESP32C3CacheState *s, hwaddr reg_addr + if (s->flash_blk != NULL) { + blk_pread(s->flash_blk, physical_address, ESP32C3_PAGE_SIZE, cache_data, 0); + } +- if (xts_aes_class->is_flash_enc_enabled(s->xts_aes)) { +- xts_aes_class->decrypt(s->xts_aes, physical_address, cache_data, ESP32C3_PAGE_SIZE); ++ // XTS-AES Flash Decryption - Only wired when built with CONFIG_GCRYPT; absent here means flash is treated as unencrypted, matching hardware with flash encryption disabled. Encrypted-flash firmware needs the gcrypt build. ++ if (s->xts_aes != NULL) { ++ ESP32C3XtsAesClass *xts_aes_class = ESP32C3_XTS_AES_GET_CLASS(s->xts_aes); ++ if (xts_aes_class->is_flash_enc_enabled(s->xts_aes)) { ++ xts_aes_class->decrypt(s->xts_aes, physical_address, cache_data, ESP32C3_PAGE_SIZE); ++ } + } + } + s->mmu[index].val = e.val; +@@ -225,12 +228,6 @@ static void esp32c3_cache_realize(DeviceState *dev, Error **errp) + /* Initialize the registers */ + esp32c3_cache_reset_hold(OBJECT(dev), RESET_TYPE_COLD); + +- ESP32C3CacheState *s = ESP32C3_CACHE(dev); +- +- /* Make sure XTS_AES was set or issue an error */ +- if (s->xts_aes == NULL) { +- error_report("[CACHE] XTS_AES controller must be set!"); +- } + } + + static void esp32c3_cache_init(Object *obj) +diff --git a/hw/misc/esp_sha.c b/hw/misc/esp_sha.c +index c9fa2e610a..cc186595eb 100644 +--- a/hw/misc/esp_sha.c ++++ b/hw/misc/esp_sha.c +@@ -22,46 +22,71 @@ + #define SHA_WARNING 0 + #define SHA_DEBUG 0 + ++static void esp_sha1_init(void *context) { sha1_init(context); } ++static void esp_sha224_init(void *context) { sha224_init(context); } ++static void esp_sha256_init(void *context) { sha256_init(context); } ++static void esp_sha384_init(void *context) { sha384_init(context); } ++static void esp_sha512_init(void *context) { sha512_init(context); } ++static void esp_sha512_224_init(void *context) { sha512_224_init(context); } ++static void esp_sha512_256_init(void *context) { sha512_256_init(context); } ++static void esp_sha512_t_init(void *context) { sha512_t_init(context); } ++static void esp_sha1_compress(void *context, const uint8_t *message) ++{ ++ sha1_compress(context, message); ++} ++static void esp_sha224_compress(void *context, const uint8_t *message) ++{ ++ sha224_compress(context, (uint8_t *)message); ++} ++static void esp_sha256_compress(void *context, const uint8_t *message) ++{ ++ sha256_compress(context, (uint8_t *)message); ++} ++static void esp_sha512_compress(void *context, const uint8_t *message) ++{ ++ sha512_compress(context, (uint8_t *)message); ++} ++ + static ESPHashAlg esp_sha_algs[] = { + [ESP_SHA_1_MODE] = { +- .init = (hash_init) sha1_init, +- .compress = (hash_compress) sha1_compress, ++ .init = esp_sha1_init, ++ .compress = esp_sha1_compress, + .len = sizeof(struct sha1_state) + }, + [ESP_SHA_224_MODE] = { +- .init = (hash_init) sha224_init, +- .compress = (hash_compress) sha224_compress, ++ .init = esp_sha224_init, ++ .compress = esp_sha224_compress, + .len = SHA224_HASH_SIZE + }, + [ESP_SHA_256_MODE] = { +- .init = (hash_init) sha256_init, +- .compress = (hash_compress) sha256_compress, ++ .init = esp_sha256_init, ++ .compress = esp_sha256_compress, + .len = sizeof(struct sha256_state) + }, + [ESP_SHA_384_MODE] = { +- .init = (hash_init) sha384_init, +- .compress = (hash_compress) sha512_compress, ++ .init = esp_sha384_init, ++ .compress = esp_sha512_compress, + .len = SHA384_HASH_SIZE + }, + [ESP_SHA_512_MODE] = { +- .init = (hash_init) sha512_init, +- .compress = (hash_compress) sha512_compress, ++ .init = esp_sha512_init, ++ .compress = esp_sha512_compress, + .len = sizeof(struct sha512_state) + }, + [ESP_SHA_512_224_MODE] = { +- .init = (hash_init) sha512_224_init, +- .compress = (hash_compress) sha512_compress, ++ .init = esp_sha512_224_init, ++ .compress = esp_sha512_compress, + .len = sizeof(struct sha512_state) + }, + [ESP_SHA_512_256_MODE] = { +- .init = (hash_init) sha512_256_init, +- .compress = (hash_compress) sha512_compress, ++ .init = esp_sha512_256_init, ++ .compress = esp_sha512_compress, + .len = sizeof(struct sha512_state) + }, + [ESP_SHA_512_t_MODE] = { +- .init = (hash_init) sha512_t_init, +- .init_message = (hash_init_message) sha512_t_init_message, +- .compress = (hash_compress) sha512_compress, ++ .init = esp_sha512_t_init, ++ .init_message = sha512_t_init_message, ++ .compress = esp_sha512_compress, + .len = sizeof(struct sha512_state) + }, + }; diff --git a/hw/riscv/esp32c3.c b/hw/riscv/esp32c3.c index 2f1ccbfbb0..febc96bfe3 100644 --- a/hw/riscv/esp32c3.c @@ -922,6 +1055,29 @@ index 43f9a43f3f..595fa47866 100644 } else { if (setgroups(1, &user_gid) < 0) { error_report("Failed to setgroups(1, [%d])", +diff --git a/qom/object.c b/qom/object.c +index 9edc06d391..c3d657e6cb 100644 +--- a/qom/object.c ++++ b/qom/object.c +@@ -1197,7 +1197,8 @@ GSList *object_class_get_list(const char *implements_type, + return list; + } + +-static gint object_class_cmp(gconstpointer a, gconstpointer b) ++static gint object_class_cmp(gconstpointer a, gconstpointer b, ++ gpointer user_data) + { + return strcasecmp(object_class_get_name((ObjectClass *)a), + object_class_get_name((ObjectClass *)b)); +@@ -1207,7 +1208,7 @@ GSList *object_class_get_list_sorted(const char *implements_type, + bool include_abstract) + { + return g_slist_sort(object_class_get_list(implements_type, include_abstract), +- object_class_cmp); ++ (GCompareFunc)object_class_cmp); + } + + Object *object_ref(void *objptr) diff --git a/scripts/meson-buildoptions.sh b/scripts/meson-buildoptions.sh index a8066aab03..bd3ae90008 100644 --- a/scripts/meson-buildoptions.sh diff --git a/scripts/build-qemu-wasm.sh b/scripts/build-qemu-wasm.sh index a3201e3..86c6a34 100755 --- a/scripts/build-qemu-wasm.sh +++ b/scripts/build-qemu-wasm.sh @@ -42,7 +42,7 @@ fi printf 'Building Emscripten environment (qemu-wasm %s)\n' "$QEMU_WASM_COMMIT" "$ENGINE" build -t "$IMAGE" -f "$ROOT/qemu/wasm/Dockerfile" "$ROOT/qemu/wasm" -FLAGS="-O3 -Wno-error=unused-command-line-argument -matomics -mbulk-memory -DNDEBUG -DG_DISABLE_ASSERT -D_GNU_SOURCE -sASYNCIFY=1 -pthread -sPROXY_TO_PTHREAD=1 -sFORCE_FILESYSTEM -sALLOW_TABLE_GROWTH -sEMULATE_FUNCTION_POINTER_CASTS=1 -sINITIAL_MEMORY=512MB -sWASM_BIGINT -sMALLOC=mimalloc -sEXPORT_ES6=1 -sASYNCIFY_IMPORTS=ffi_call_js" +FLAGS="-O3 -Wno-error=unused-command-line-argument -matomics -mbulk-memory -DNDEBUG -DG_DISABLE_ASSERT -D_GNU_SOURCE -sASYNCIFY=1 -pthread -sPROXY_TO_PTHREAD=1 -sFORCE_FILESYSTEM -sALLOW_TABLE_GROWTH -sINITIAL_MEMORY=512MB -sWASM_BIGINT -sMALLOC=mimalloc -sEXPORT_ES6=1 -sASYNCIFY_IMPORTS=ffi_call_js" if [ ! -f build/build.ninja ]; then "$ENGINE" run --rm -v "$SRC:/qemu" "$IMAGE" bash -lc " @@ -76,6 +76,8 @@ install -m 0644 build/qemu-system-riscv32.worker.js "$OUT/" install -m 0644 pc-bios/esp32c3-rom.bin "$OUT/" if command -v node >/dev/null; then "$ROOT/scripts/smoke-qemu-wasm.sh" "$OUT/qemu-system-riscv32.js" - node "$ROOT/scripts/smoke-browser-bridge.mjs" "$OUT" + # Retry Bridge Smoke - Emscripten pthread startup has a rare transient trap during proxied entry; a second attempt clears it. Fail only if it never succeeds. + node "$ROOT/scripts/smoke-browser-bridge.mjs" "$OUT" \ + || node "$ROOT/scripts/smoke-browser-bridge.mjs" "$OUT" fi printf '%s\n' "$OUT"