[Git][ghc/ghc][master] wasm: workaround WebKit bug in dyld
Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC Commits: c9b8465c by Cheng Shao at 2025-10-20T10:16:00-04:00 wasm: workaround WebKit bug in dyld This patch works around a WebKit bug and allows dyld to run on WebKit based platforms as well. See added note for detailed explanation. Co-authored-by: Codex <codex@openai.com> - - - - - 1 changed file: - utils/jsffi/dyld.mjs Changes: ===================================== utils/jsffi/dyld.mjs ===================================== @@ -670,7 +670,25 @@ class DyLD { // Wasm memory & table #memory = new WebAssembly.Memory({ initial: 1 }); + #table = new WebAssembly.Table({ element: "anyfunc", initial: 1 }); + // First free slot, might be invalid when it advances to #table.length + #tableFree = 1; + // See Note [The evil wasm table grower] + #tableGrowInstance = new WebAssembly.Instance( + new WebAssembly.Module( + new Uint8Array([ + 0, 97, 115, 109, 1, 0, 0, 0, 1, 6, 1, 96, 1, 127, 1, 127, 2, 35, 1, 3, + 101, 110, 118, 25, 95, 95, 105, 110, 100, 105, 114, 101, 99, 116, 95, + 102, 117, 110, 99, 116, 105, 111, 110, 95, 116, 97, 98, 108, 101, 1, + 112, 0, 0, 3, 2, 1, 0, 7, 31, 1, 27, 95, 95, 103, 104, 99, 95, 119, 97, + 115, 109, 95, 106, 115, 102, 102, 105, 95, 116, 97, 98, 108, 101, 95, + 103, 114, 111, 119, 0, 0, 10, 11, 1, 9, 0, 208, 112, 32, 0, 252, 15, 0, + 11, + ]) + ), + { env: { __indirect_function_table: this.#table } } + ); // __stack_pointer #sp = new WebAssembly.Global( @@ -715,6 +733,82 @@ class DyLD { // Global STG registers #regs = {}; + // Note [The evil wasm table grower] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // We need to grow the wasm table as we load shared libraries in + // wasm dyld. We used to directly call the table.grow() JS API, + // which works as expected in Firefox/Chrome, but unfortunately, + // WebKit's implementation of the table.grow() JS API is broken: + // https://bugs.webkit.org/show_bug.cgi?id=290681, which means that + // the wasm dyld simply does not work in WebKit-based browsers like + // Safari. + // + // Now, one simple workaround would be to avoid growing the table at + // all: just allocate a huge table upfront (current limitation + // agreed by all vendors is 10000000). To avoid unnecessary space + // waste on non-WebKit platforms, we could additionally check + // navigator.userAgent against some regexes and only allocate + // fixed-length table when there's no blink/gecko mention. But this + // is fragile and gross, and it's better to stick to a uniform code + // path for all browsers. + // + // Fortunately, it turns out the table.grow wasm instruction work as + // expected in WebKit! So we can invoke a wasm function that grows + // the table for us. But don't open a champagne yet, where would + // that wasm function come from? It can't be put into RTS, or even + // libc.so, because loading those libraries would require growing + // the table in the first place! Or perhaps, reserve a table upfront + // that's just large enough to load RTS and then we can access that + // function for subsequent table grows? But then we need to + // experiment for a reasonable initial size, and add a magic number + // here, which is also fragile and gross and not future-proof! + // + // So this special wasm function needs to live in a single wasm + // module, which is loaded before we load anything else. The full + // source code for this module is: + // + // (module + // (type (func (param i32) (result i32))) + // (import "env" "__indirect_function_table" (table 0 funcref)) + // (export "__ghc_wasm_jsffi_table_grow" (func 0)) + // (func (type 0) (param i32) (result i32) + // ref.null func + // local.get 0 + // table.grow 0 + // ) + // ) + // + // This module is 103 bytes so that we can inline its blob in dyld, + // and use the usually discouraged synchronous + // WebAssembly.Instance/WebAssembly.Module constructors to load it. + // On non-WebKit platforms, growing tables this way would introduce + // a bit of extra JS/Wasm interop overhead, which can be amplified + // as we used to call table.grow(1, foo) for every GOT.func item. + // Therefore, unless we're about to exceed the hard limit of table + // size, we now grow the table exponentially, and use bump + // allocation to calculate the table index to be returned. + // Exponential growth is only implemented to minimize the JS/Wasm + // interop overhead when calling __ghc_wasm_jsffi_table_grow; + // V8/SpiderMonkey/WebKit already do their own exponential growth of + // the table's backing buffer in their table growth logic. + // + // Invariants: n >= 0; when v is non-null, n === 1 + #tableGrow(n, v) { + const prev_free = this.#tableFree; + if (prev_free + n > this.#table.length) { + const min_delta = prev_free + n - this.#table.length; + const delta = Math.max(min_delta, this.#table.length); + this.#tableGrowInstance.exports.__ghc_wasm_jsffi_table_grow( + this.#table.length + delta <= 10000000 ? delta : min_delta + ); + } + if (v) { + this.#table.set(prev_free, v); + } + this.#tableFree += n; + return prev_free; + } + constructor({ args, rpc }) { this.#rpc = rpc; @@ -878,7 +972,7 @@ class DyLD { // __memory_base & __table_base, different for each .so let memory_base; - let table_base = this.#table.grow(tableSize); + let table_base = this.#tableGrow(tableSize); console.assert(tableP2Align === 0); // libc.so is always the first one to be ever loaded and has VIP @@ -982,7 +1076,7 @@ class DyLD { if (this.exportFuncs[name]) { this.#gotFunc[name] = new WebAssembly.Global( { value: "i32", mutable: true }, - this.#table.grow(1, this.exportFuncs[name]) + this.#tableGrow(1, this.exportFuncs[name]) ); continue; } @@ -1033,7 +1127,7 @@ class DyLD { if (this.#gotFunc[k]) { const got = this.#gotFunc[k]; if (got.value === DyLD.#poison) { - const idx = this.#table.grow(1, v); + const idx = this.#tableGrow(1, v); got.value = idx; } else { this.#table.set(got.value, v); @@ -1103,7 +1197,7 @@ class DyLD { // Not in GOT.func yet, create the entry on demand if (this.exportFuncs[sym]) { console.assert(!this.#gotFunc[sym]); - const addr = this.#table.grow(1, this.exportFuncs[sym]); + const addr = this.#tableGrow(1, this.exportFuncs[sym]); this.#gotFunc[sym] = new WebAssembly.Global( { value: "i32", mutable: true }, addr View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/c9b8465c2c338176fcab9d197e9d31f4... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/c9b8465c2c338176fcab9d197e9d31f4... You're receiving this email because of your account on gitlab.haskell.org.
participants (1)
-
Marge Bot (@marge-bot)