From 778aa450d9b768702bffcab09bfb618c2b2bef22 Mon Sep 17 00:00:00 2001 From: uwezkhan Date: Mon, 20 Jul 2026 14:56:04 +0530 Subject: [PATCH] Encode i32.const immediates as signed LEB128 in the PostScript Wasm compiler The Type-4 PostScript -> Wasm compiler emitted i32.const immediates with the unsigned LEB128 encoder (_emitULEB128). Wasm decodes i32.const as a *signed* LEB128, so any immediate whose final 7-bit group has bit 0x40 set is mis-decoded (e.g. 64 -> single byte 0x40 -> read back as -64). The output-store address for the i-th result is emitted as i32.const (i*8). For a function with >= 9 outputs the 9th offset is 64, which decodes as -64, so f64.store targets 0xFFFFFFC0 and traps (memory access out of bounds). The module still validates and instantiates, so the JS fallback in function.js is not engaged; the trap only surfaces at render time (the affected shading / tint transform silently fails to render). The same mis-encoding affects the constant bitshift amount. Add a signed-LEB128 emitter (_emitSLEB128) and use it for the three i32.const immediate sites; local/import/type indices remain unsigned (correct). Add a unit test covering functions with 9+ outputs, and a reference test rendering a shading whose colour function has 9 outputs. --- src/core/postscript/wasm_compiler.js | 21 +++++++++++++++++--- test/pdfs/.gitignore | 1 + test/pdfs/postscript_type4_many_outputs.pdf | Bin 0 -> 1117 bytes test/test_manifest.json | 7 +++++++ test/unit/postscript_spec.js | 20 +++++++++++++++++++ 5 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 test/pdfs/postscript_type4_many_outputs.pdf diff --git a/src/core/postscript/wasm_compiler.js b/src/core/postscript/wasm_compiler.js index 1b552c3b877f2..bdc7b555fb9e5 100644 --- a/src/core/postscript/wasm_compiler.js +++ b/src/core/postscript/wasm_compiler.js @@ -283,6 +283,21 @@ class PsWasmCompiler { } while (n !== 0); } + // `i32.const` immediates are signed LEB128 (Wasm spec), so they must be + // emitted with sign extension — the unsigned encoder mis-encodes any value + // whose final 7-bit group has bit 0x40 set (e.g. 64 → 0x40 → decoded as −64). + _emitSLEB128(n) { + for (;;) { + const b = n & 0x7f; + n >>= 7; // arithmetic shift keeps the sign bit + if ((n === 0 && (b & 0x40) === 0) || (n === -1 && (b & 0x40) !== 0)) { + this._code.push(b); + return; + } + this._code.push(b | 0x80); + } + } + _emitF64Const(value) { this._code.push(OP.f64_const); PsWasmCompiler.#f64View.setFloat64(0, value, true /* little-endian */); @@ -532,11 +547,11 @@ class PsWasmCompiler { const shift = first.value; if (shift > 0) { code.push(OP.i32_const); - this._emitULEB128(shift); + this._emitSLEB128(shift); code.push(OP.i32_shl); } else if (shift < 0) { code.push(OP.i32_const); - this._emitULEB128(-shift); + this._emitSLEB128(-shift); code.push(OP.i32_shr_s); } code.push(OP.f64_convert_i32_s); @@ -870,7 +885,7 @@ class PsWasmCompiler { const min = this._range[i * 2]; const max = this._range[i * 2 + 1]; code.push(OP.i32_const); - this._emitULEB128(i * 8); + this._emitSLEB128(i * 8); if (!this._compileNode(outputs[i])) { return null; } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index e25516a81e193..2d5724f6e7f2c 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -1,6 +1,7 @@ *.pdf *.error +!postscript_type4_many_outputs.pdf !boundingBox_invalid.pdf !pdkids.pdf !tracemonkey.pdf diff --git a/test/pdfs/postscript_type4_many_outputs.pdf b/test/pdfs/postscript_type4_many_outputs.pdf new file mode 100644 index 0000000000000000000000000000000000000000..23d775b4490a4bbc57afd591e4a1293b3f676d7a GIT binary patch literal 1117 zcma)6U2oGc6n)RHxG#N#*h!tV(zFNG^<@l3i!`CA4<_-_pw14Cg9;PBaS!|h_#?a5 zO-k2{iYTW_j_=1g*KS5Pv-vCjIt@m@|2+N*I504uzXWGz5Ptk#D+n*7lN(b)cq2>I zK!gDcI6n`RE+~-RpC|0%!mFxiAd4w{g;h+e9q@xhS{Wu%8>=vM*C)JIMJ3;uEo2Pi zBj$ni7OF9=%~233U9M$O>9Tv~Fs829i*9VK9ad~WbZjr3qIv~g1#dZ`oQ<)|TILE8 zpM{LEWT=HVn*wY zvr66vUvYi!7p#I?(BzXtvxX1kpbPA_fmBkxES#xtRj%Ga7Um4XoFmXs9wWvGfk+T3 zVnQB#_{H_7E0|!7#j2kUx#$)4*8F&|%5&ojsY~UjfGKo<{t4{Yn-vFUAE0RKllVDw zOlto_-zhn?hLT>^|5xkSAKPx;4^W%>BxvIpyr6h%)m^|~8nFEZi4d`XJJ@p^PX=AM z-Fq&9F2d3^@_wejn9KrU#s}vf3!?KMc$2hk_{%&KUYncOQht QN$b4FNh*TT=<@yi2@k+1%>V!Z literal 0 HcmV?d00001 diff --git a/test/test_manifest.json b/test/test_manifest.json index c60a97fdfdd90..59433084a8f7c 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -1,4 +1,11 @@ [ + { + "id": "postscript-type4-many-outputs", + "file": "pdfs/postscript_type4_many_outputs.pdf", + "md5": "90c1ebd35acc6c7d01f4fdb0ae0bef82", + "rounds": 1, + "type": "eq" + }, { "id": "bug1755201", "file": "pdfs/bug1755201.pdf", diff --git a/test/unit/postscript_spec.js b/test/unit/postscript_spec.js index cdfdcc0b5d488..b58c277bab15e 100644 --- a/test/unit/postscript_spec.js +++ b/test/unit/postscript_spec.js @@ -1049,6 +1049,26 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () { ); expect(r2).toBeCloseTo(0.5, 9); }); + + it("compiles functions with 9+ outputs (signed i32.const store offset)", async function () { + // Regression: each output's f64.store address is emitted as + // `i32.const (i * 8)`. i32.const immediates are *signed* LEB128, so the + // 9th output offset (64) must not be written with the unsigned encoder, + // which yields the byte 0x40 that Wasm decodes as -64 → out-of-bounds + // store → runtime trap. compileAndRun throws if the Wasm function traps. + for (const nOut of [9, 10, 16, 20]) { + const range = []; + for (let i = 0; i < nOut; i++) { + range.push(0, 1000); + } + const src = "{" + " dup".repeat(nOut - 1) + " }"; + const out = compileAndRun(src, [0, 1], range, [0.5]); + expect(out.length).toBe(nOut); + for (const value of out) { + expect(value).toBeCloseTo(0.5, 10); + } + } + }); }); // PSStackToTree