After three years, I have solved the mystery why my webassembly won't load correctly as mentioned in Negative Results, itself a follow-up to Shared Array Buffers with WASM. I asked an AI.
It turns out we needed to explicitly set the stack memory location during setup, so that each program got separate stack memory to work with. To do this, and allocate thread-local storage areas as well, we add --export=__stack_pointer --export=__wasm_init_tls --export=__tls_size --export=__tls_base --export=__tls_align to the Rust link args, and then set them like this:
const REGION_BASE = 2 * 1024 * 1024 //Above the module's static data, heap base, and world.globalTick.
const STACK_SIZE = 512 * 1024 //Plenty for this program; grows downward from the top of the block.
const align = n => (n + 15) & ~15
const tlsSize = align(exports.__tls_size.value) //0 here (no #[thread_local]s), but honour it for correctness.
const blockSize = tlsSize + STACK_SIZE
const blockBase = REGION_BASE + workerIndex * blockSize
//TLS sits at the bottom of the block; the shadow stack occupies the rest and grows down from the top.
//Set the private shadow stack FIRST, before any wasm call (including TLS init), so the
//very first function entry runs on this worker's own stack rather than the shared one.
exports.__stack_pointer.value = blockBase + blockSize
exports.__wasm_init_tls(blockBase)
The minimal diff on top of my minimal test-case to fix it shows exactly how to do this, if you have run into the same solution and need a fix.
However, this is not directly what the AI produced. What it did is quite interesting, and kind of reinforces the "incredibly brilliant, incredibly stupid" bimodality of these expert systems. The first patch it produced fixed the issue, but by patching the compiled WASM binary the worker ran, instead of patching the source code right next to it in the folder. The fix was another prompt to "not do that" producing a saner patch, but my goodness… brilliant but blind. 🤦