Moteur WebAssembly : remettre la pile en place après chaque commande

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 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-03 00:27:24 +01:00
co-authored by Claude Opus 5.5
parent a8636b3f83
commit ae1ea54782
3 changed files with 54 additions and 4 deletions
+37
View File
@@ -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}`);
}
});
+6 -2
View File
@@ -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 \
+11 -2
View File
@@ -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) {