diff --git a/ChangeLog.md b/ChangeLog.md index 99d84f7ffb075..d95ba68965b5a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,9 @@ See docs/process.md for more on how version tagging works. 6.0.3 (in development) ---------------------- +- Fixed `UTF8ToString` with `GROWABLE_ARRAYBUFFERS` set. String decoding now + copies the data when the heap buffer is resizable, just like it does in + the shared memory case. (#27242) - Added support for compiling FMA intrinsics. All 32 FMA intrinsics are supported, with 256-bit variants emulated via two 128-bit operations. Pass ``-msimd128 -mfma`` to enable. With ``-mrelaxed-simd -mfma``, Wasm relaxed diff --git a/src/parseTools.mjs b/src/parseTools.mjs index 1826aa945e17b..aec07df224c3e 100644 --- a/src/parseTools.mjs +++ b/src/parseTools.mjs @@ -1081,27 +1081,41 @@ function runtimeKeepalivePop() { return 'runtimeKeepalivePop();'; } -// Some web functions like TextDecoder.decode() may not work with a view of a -// SharedArrayBuffer, see https://github.com/whatwg/encoding/issues/172 -// To avoid that, this function allows obtaining an unshared copy of an -// ArrayBuffer. +// Some web functions like TextDecoder.decode() do not work with a view of a +// SharedArrayBuffer (see https://github.com/whatwg/encoding/issues/172) or of +// a resizable ArrayBuffer (see +// https://github.com/emscripten-core/emscripten/issues/27241). +// To avoid that, this function allows obtaining a copy in those cases. function getUnsharedTextDecoderView(heap, start, end) { - const shared = `${heap}.slice(${start}, ${end})`; - const unshared = `${heap}.subarray(${start}, ${end})`; + const copy = `${heap}.slice(${start}, ${end})`; + const view = `${heap}.subarray(${start}, ${end})`; - // No need to worry about this in non-shared memory builds - if (!SHARED_MEMORY) return unshared; + // No need to worry about this in builds where the buffer can be neither + // shared nor resizable. + if (!SHARED_MEMORY && !(ALLOW_MEMORY_GROWTH && GROWABLE_ARRAYBUFFERS)) return view; - // If asked to get an unshared view to what we know will be a shared view, or - // if in -Oz, then unconditionally do a .slice() for smallest code size. + // If in -Oz, then unconditionally do a .slice() for smallest code size. // This is guaranteed to work but could be slower since it performs a copy. - if (SHRINK_LEVEL == 2 || heap.startsWith('HEAP')) return shared; - - // Otherwise, generate a runtime type check: must do a .slice() if looking at - // a SAB, or can use .subarray() otherwise. Note: We compare with - // `ArrayBuffer` here to avoid referencing `SharedArrayBuffer` which could be - // undefined. - return `${heap}.buffer instanceof ArrayBuffer ? ${unshared} : ${shared}`; + if (SHRINK_LEVEL == 2) return copy; + + if (SHARED_MEMORY) { + // If asked to get an unshared view to what we know will be a shared view, + // then unconditionally do a .slice(). + if (heap.startsWith('HEAP')) return copy; + + // Otherwise, generate a runtime type check: must do a .slice() if looking + // at a SAB, or can use .subarray() otherwise. Note: We compare with + // `ArrayBuffer` here to avoid referencing `SharedArrayBuffer` which could + // be undefined. + return `${heap}.buffer instanceof ArrayBuffer ? ${view} : ${copy}`; + } + + // With GROWABLE_ARRAYBUFFERS == 2 the heap is always resizable; with + // GROWABLE_ARRAYBUFFERS == 1 resizability is feature-detected at runtime, + // and non-heap views passed to UTF8ArrayToString may not be resizable at + // all, so generate a runtime check in those cases. + if (GROWABLE_ARRAYBUFFERS == 2 && heap.startsWith('HEAP')) return copy; + return `${heap}.buffer.resizable ? ${copy} : ${view}`; } function getEntryFunction() { diff --git a/test/test_other.py b/test/test_other.py index 8b1eb9b4e209b..8104e574f4df7 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15537,6 +15537,40 @@ def test_TextDecoder_invalid(self): expected = '#error "TEXTDECODER must be either 1 or 2"' self.assert_fail([EMCC, test_file('hello_world.c'), '-sTEXTDECODER=3'], expected) + # Verify that strings can be decoded when the heap is backed by a resizable + # ArrayBuffer (ALLOW_MEMORY_GROWTH + GROWABLE_ARRAYBUFFERS). + # TextDecoder.decode() rejects views of resizable ArrayBuffers, so we must + # pass it a copy of the data instead. Node's TextDecoder happens to accept + # such views, so emulate the browser behaviour in a --pre-js. + # See https://github.com/emscripten-core/emscripten/issues/27241 + @requires_node_26 + @parameterized({ + '': ([],), + 'textdecoder_2': (['-sTEXTDECODER=2'],), + }) + def test_TextDecoder_resizable_buffer(self, args): + create_file('pre.js', ''' + var realDecode = TextDecoder.prototype.decode; + TextDecoder.prototype.decode = function(data) { + if (data && data.buffer && (data.buffer.resizable || data.buffer.growable)) { + throw new TypeError('The provided ArrayBuffer value must not be resizable'); + } + return realDecode.call(this, data); + }; + ''') + create_file('main.c', r''' + #include + + int main() { + // Long enough (> 16 bytes) that UTF8ToString on the string takes the + // TextDecoder path. + emscripten_out("the quick brown fox jumps over the lazy dog"); + return 0; + } + ''') + self.do_runf('main.c', 'the quick brown fox jumps over the lazy dog', + cflags=['--pre-js=pre.js', '-sALLOW_MEMORY_GROWTH', '-sGROWABLE_ARRAYBUFFERS=1'] + args) + def test_reallocarray(self): self.do_other_test('test_reallocarray.c')