From ae1ea547822ecbf1cdb8cc1e6b89be91b6acc222 Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Tue, 3 Nov 2026 00:27:24 +0100 Subject: [PATCH] =?UTF-8?q?Moteur=20WebAssembly=20:=20remettre=20la=20pile?= =?UTF-8?q?=20en=20place=20apr=C3=A8s=20chaque=20commande?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit callMain (Emscripten) copie les arguments sur la pile du WebAssembly (64 Ko) sans les retirer. Pendant une partie dans le navigateur, le même moteur reçoit des centaines de commandes : vers la 540e, la pile débordait et corrompait la mémoire du moteur (moteur muet dans Firefox, onglet planté dans Chromium, « memory access out of bounds » sous Node). Repéré par make test-browser. stackSave et stackRestore sont exportés et encadrent chaque commande ; un test en envoie 3000 à la suite. Co-Authored-By: Claude Opus 5.5 --- app/tests/engine-stack.test.ts | 37 ++++++++++++++++++++++++++++++++++ engine-wasm/wasm.mk | 8 ++++++-- web/engine-wasm.mjs | 13 ++++++++++-- 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 app/tests/engine-stack.test.ts diff --git a/app/tests/engine-stack.test.ts b/app/tests/engine-stack.test.ts new file mode 100644 index 0000000..189cf44 --- /dev/null +++ b/app/tests/engine-stack.test.ts @@ -0,0 +1,37 @@ +// Le moteur WebAssembly répond indéfiniment : une partie en navigateur +// envoie des centaines de commandes au même moteur chargé une fois. +// +// Copyright (C) 2026 les auteurs d'Ustagammon. +// Licence : GPLv3 ou ultérieure (voir LICENSE). +// +// Régression : callMain (Emscripten) laisse ses arguments sur la pile du +// WebAssembly (64 Ko). Sans remise en place après chaque commande +// (web/engine-wasm.mjs), la pile débordait vers la 500e commande : erreur +// « memory access out of bounds » sous Node, moteur muet dans Firefox, +// onglet planté dans Chromium. + +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +// Compilé dans build/web/tests/ : la racine du dépôt est trois dossiers plus +// haut. +const root = new URL("../../../", import.meta.url); + +interface RawEngine { + runText(args: string[]): { text: string; exitCode: number }; +} + +test("le moteur répond à 3000 commandes à la suite", async () => { + const module = await import(new URL("build/wasm/engine.mjs", root).href); + const wrapper = await import(new URL("web/engine-wasm.mjs", root).href); + const engine: RawEngine = await wrapper.loadEngine( + module.default, + fileURLToPath(new URL("build/wasm/", root)), + ); + for (let i = 0; i < 3000; i++) { + const { text, exitCode } = engine.runText(["position", "4HPwATDgc/ABMA", "AQEAAAAAAAAA"]); + assert.equal(exitCode, 0, `commande ${i}`); + assert.match(text, /"pip_count": 167/, `commande ${i}`); + } +}); diff --git a/engine-wasm/wasm.mk b/engine-wasm/wasm.mk index 478333e..59b7a2f 100644 --- a/engine-wasm/wasm.mk +++ b/engine-wasm/wasm.mk @@ -33,14 +33,18 @@ ENGINE_INCLUDES := -I$(COMPAT) -I$(COMPAT)/glib-shim -I$(GNUBG) -I$(GNUBG)/lib - # - main n'est pas lancé au chargement, et le programme ne s'arrête pas à sa # sortie : le moteur reste chargé entre deux commandes ; # - mémoire extensible : les bases de fin de partie occupent plusieurs -# mégaoctets. +# mégaoctets ; +# - stackSave et stackRestore : callMain laisse ses arguments sur la pile +# (64 Ko) ; sans restauration après chaque commande, elle déborde au bout +# de quelques centaines de commandes et corrompt la mémoire du moteur +# (voir web/engine-wasm.mjs). LINK_FLAGS := -O3 \ -sMODULARIZE -sEXPORT_ES6 -sEXPORT_NAME=createEngineModule \ -sENVIRONMENT=web,worker,node \ -sINVOKE_RUN=0 -sEXIT_RUNTIME=0 \ -sALLOW_MEMORY_GROWTH \ -sEXPORTED_FUNCTIONS=_main,_engine_init \ - -sEXPORTED_RUNTIME_METHODS=callMain,ccall,HEAPU8 \ + -sEXPORTED_RUNTIME_METHODS=callMain,ccall,HEAPU8,stackSave,stackRestore \ --preload-file $(BUILD)/data@/data DATA := $(BUILD)/data/gnubg.wd $(BUILD)/data/gnubg_os0.bd $(BUILD)/data/gnubg_ts0.bd \ diff --git a/web/engine-wasm.mjs b/web/engine-wasm.mjs index 1b0fca7..3430d27 100644 --- a/web/engine-wasm.mjs +++ b/web/engine-wasm.mjs @@ -36,10 +36,19 @@ export async function loadEngine(createEngineModule, wasmDir) { throw new Error("le moteur n'a pas pu charger ses données (code " + status + ")"); } + // callMain copie les arguments sur la pile du WebAssembly sans jamais + // les retirer : on remet la pile en place après chaque commande, sinon + // elle déborde au bout de quelques centaines de commandes (environ 540 + // pendant une partie) et le moteur plante ou ne répond plus. function runText(args) { output = []; - const exitCode = module.callMain(args.map(String)); - return { text: output.join("\n"), exitCode }; + const stack = module.stackSave(); + try { + const exitCode = module.callMain(args.map(String)); + return { text: output.join("\n"), exitCode }; + } finally { + module.stackRestore(stack); + } } function run(args) {