How to Fix: instantiate fuzzbug: Using a sunk load’s result in a register
A miscompiled value flowing from a sunk load into a live register is the kind of backend bug that looks random in fuzzing, but it usually points to one thing: the compiler moved or eliminated a memory read without preserving the constraints that made that read valid. In the instantiate fuzzbug: Using a sunk load’s result in a register issue, the failure is triggered during code generation when a load result that should only exist at a specific memory state is later reused as if it were still correct after scheduling, sinking, or register allocation.
Table of Contents
If you are investigating this in a compiler pipeline, the practical fix is not to treat it as a generic AddressSanitizer crash. Treat it as a load sinking and register liveness correctness bug in the lowering or scheduling pipeline, then verify all assumptions around memory dependencies, control equivalence, and value rematerialization.
Understanding the Root Cause
This bug happens when the compiler takes a load instruction from one point in the intermediate representation and sinks it to a later position, often to reduce register pressure or improve scheduling. That transformation is only valid if the following remain true:
- The memory location being loaded has not changed.
- The new placement dominates every use of the loaded value.
- The load is still valid on every control-flow path where the result is consumed.
- Any aliasing stores, calls, traps, guards, or deoptimization points are respected.
The crash appears when one of those conditions is violated and the backend still allows the result to be materialized in a physical or virtual register. At that point, later passes assume the register contains a semantically valid value, but the value may actually correspond to:
- A load from the wrong memory version.
- A load moved across a side-effecting instruction.
- A load that no longer dominates its use.
- A value that should have been recomputed, not reused.
In compiler terms, this is often a mismatch between memory SSA, effect chains, or schedule dependencies on one side and register allocation or instruction selection on the other. The fuzz case exposes that the compiler backend believes a loaded value is safely reusable, while the actual program state says otherwise.
A typical failure pattern looks like this:
- A load is marked movable or sinkable.
- The load is relocated below another instruction, block boundary, or effectful node.
- The original memory dependency is weakened or lost.
- A later use receives the old result in a register.
- The generated machine code reads invalid state, triggering an assertion, sanitizer abort, or incorrect execution.
This is why the bug title specifically mentions using a sunk load’s result in a register. The real issue is not the register itself; the register only makes the invalid optimization observable.
Step-by-Step Solution
The safest way to fix this class of issue is to tighten the legality checks for load sinking and ensure register uses cannot outlive the memory semantics of the load.
1. Reproduce the fuzz case with maximum compiler diagnostics enabled
asan_symbolize=1 ./your_compiler --trace --verify-ir --verify-schedule --verify-regalloc testcase.js
Add any project-specific verification flags that dump:
- IR before and after load sinking
- Memory/effect chains
- Block scheduling
- Liveness intervals
- Register allocation decisions
2. Identify the load that was sunk
Search the optimization log for the load node whose result is consumed after movement. Compare the original position and the final scheduled position.
// Before sinking
v42 = Load(base, offset, effect=v30, control=b1)
v43 = Use(v42)
// After sinking
v43 = Use(v42)
v42 = Load(base, offset, effect=v30, control=b2) // invalid if b2 does not dominate Use
If the load moved below a store, call, or merge point, that is your first red flag.
3. Verify domination and control equivalence
Any sunk load must still dominate every use. If your compiler supports explicit checks, add one immediately after the sinking pass.
for (LoadNode load : sunk_loads) {
for (Use use : load.uses()) {
CHECK(Dominates(load.block(), use.block()));
CHECK(IsControlEquivalentOrStronger(load, use));
}
}
If a use can be reached along a path where the sunk load is not executed, the optimization is invalid.
4. Revalidate memory dependencies and aliasing
The load must not move across anything that can modify the same memory or invalidate assumptions about it.
bool CanSinkLoad(LoadNode load, Block target) {
for (Instruction inst : InstructionsBetween(load.block(), target)) {
if (inst.HasSideEffects()) return false;
if (MayAlias(inst, load)) return false;
if (inst.IsCall()) return false;
if (inst.CanTrap()) return false;
}
return true;
}
If your IR has effect chains, ensure the sunk load is reattached to the correct effect input instead of preserving a stale one.
5. Prevent stale value reuse in register allocation
Even if scheduling survives, regalloc must not treat the load result as freely reusable if the value is path-sensitive or tied to a rematerialization constraint.
if (value.IsSunkLoadResult()) {
if (!value.HasStableMemoryVersion()) {
value.MarkNotRematerializable();
value.RequireMaterializationAtUse();
}
}
This is especially important if your backend supports common subexpression elimination, rematerialization, or late load folding.
6. Add a verifier for sunk loads
Create a dedicated validation pass that runs after scheduling and before code emission.
void VerifySunkLoads(Function fn) {
for (auto& load : fn.loads()) {
if (!load.WasSunk()) continue;
CHECK(load.HasValidEffectDependency());
CHECK(!CrossesAliasingStore(load));
CHECK(!CrossesUnsafeCall(load));
CHECK(AllUsesDominatedBy(load));
CHECK(RegisterUsesMatchLoadLifetime(load));
}
}
7. Add a regression test from the fuzz case
Reduce the OSS-Fuzz input to the smallest case that still triggers the invalid transformation. Then lock it in with a compiler test.
// RUN: your_compiler --verify-ir --verify-schedule %s
function trigger(a, b) {
let x = a[0];
if (b) {
sideEffect(a);
}
return x + 1;
}
// CHECK: no sunk load reused across side effect
8. Prefer disabling the optimization over shipping an unsound transformation
If the precise fix is complex, gate load sinking more conservatively for the affected pattern.
if (load.IsHeapLoad() && region.HasUnknownSideEffects()) {
return DontSink;
}
This is often the right immediate fix for security-sensitive compiler code until a broader proof of correctness is implemented.
Common Edge Cases
- Calls with hidden side effects: Runtime helpers, GC safepoints, and foreign calls may mutate memory even when they look pure at a high level.
- Exception or trap edges: A load moved across an instruction that can throw may no longer execute on all paths that use its value.
- Phi or merge nodes: Sinking into a successor block can break dominance when uses exist on sibling or merged paths.
- Alias analysis gaps: If alias analysis says two references do not overlap but that assumption is incomplete, the sink becomes unsound.
- Deoptimization metadata: Some JITs must preserve the exact value at a safepoint. A sunk load can make deopt state inconsistent.
- Load folding in instruction selection: A value may appear safe in IR but become dangerous when folded into a machine instruction that has stricter placement rules.
- GC-moving objects: In managed runtimes, a pointer-derived load reused after a safepoint can be stale if relocation rules are not modeled correctly.
FAQ
1. Why does this show up as an AddressSanitizer abort instead of a clean compiler assertion?
Because the invalid transformation often survives multiple backend stages before becoming observable. By the time machine code or low-level runtime data is touched, the corrupted assumption manifests as invalid memory access, poisoned state, or an internal abort detected by sanitizers.
2. Is the bug in register allocation or in load sinking?
Usually the root cause is in load sinking legality or missing memory-dependence tracking. Register allocation is where the bad value becomes concretely reused, so it exposes the issue but is not always the original source.
3. What is the safest short-term patch if I cannot fully prove the optimization?
Disable or narrow the optimization for the suspicious load class, especially across calls, stores, merges, safepoints, or unknown side effects. A conservative compiler is far better than a fast but unsound backend.
For ongoing investigation, keep the original fuzz reproducer linked in your internal bug tracker and map it to a permanent regression test derived from the OSS-Fuzz testcase. That gives you a concrete way to verify both the immediate fix and any future optimizer changes that touch memory motion, effect ordering, or register value reuse.