← Back to AILP Home

Cross-region safety — ARIA-029 & ARIA-031 in practice

This chapter is the practical companion to the diagnostic reference in diagnostics.md. It walks through the two cross-region reference hazardsARIA-029 (gc-ref-from-wild) and ARIA-031 (stack-ref-into-gc-field) — explains the exact recogniser shapes that trigger them, and shows the documented workarounds with runnable patterns.

The two diagnostics were shipped in v0.27.2 (ARIA-029) and v0.27.3 (ARIA-031); v0.31.3.2 D-25 extended the recognisers to walk struct field paths. v0.31.12.x added coverage fixtures (bug546bug560) and the K-runtime parity tests (K core 168 + 169) that lock the workaround patterns.

The two hazards in one paragraph

Cross-region writes are unsafe when the destination outlives the source's region contract. A wild pointer that captures a gc reference can outlive the GC's reachability graph (the next collection frees the object out from under the pointer). A gc struct field that captures a stack reference can outlive the stack frame that owns the source (the field dangles the moment the callee returns). Both are detected at the store site, not the load.

flowchart LR
    A[gc T:g = ...] -- "@g.field" --> B[wild T->:p]
    B -- ARIA-029 --> X[(use-after-collect)]
    C[stack T:s = ...] -- "@s.field" --> D["gc Holder:h<br/>(h.p)"]
    D -- ARIA-031 --> Y[(use-after-pop)]

ARIA-029 — GC_REF_FROM_WILD

Trigger shape

gc Leaf:l = Leaf{ value: 7i32 };
wild Leaf->:p = @l;          // ARIA-029 — gc-rooted RHS into wild slot

The recogniser is path-rooted: it fires when the RHS resolves to a gc binding at any field depth, not only on a bare gc identifier. The 3-level case is identical in shape:

gc Outer:o = Outer{ mid: Mid{ leaf: Leaf{ value: 7i32 } } };
wild Leaf->:p = @o.mid.leaf;   // ARIA-029

This is the contract pinned by bug546 (deep field path).

What does not trigger ARIA-029

nitpick gc Holder:h = Holder{ value: 7i32 }; wild ?->:fresh = npk_alloc(16i64); // OK — unrelated npk_free(fresh);

Workarounds

Copy-by-value (bug547) — extract the value out of the gc graph into a local; the local is a fresh stack copy and is no longer "the gc object's storage":

gc Outer:o = Outer{ mid: Mid{ leaf: Leaf{ value: 7i32 } } };
Leaf:copy = o.mid.leaf;
exit copy.value;        // exit 7

Pin escape hatch (bug548) — pin the gc root with # so the GC keeps it alive through the wild reference:

gc Outer:o = Outer{ mid: Mid{ leaf: Leaf{ value: 7i32 } } };
wild Outer->:po = #o;
exit po->mid.leaf.value;   // exit 7

Composition (bug550) — both workarounds compose in the same function (copy one branch, pin another).

K core 168 (168_nitpick029_copy_workaround_value_flow_pass.npk) locks the runtime value flow of the copy-by-value pattern; the C++ runtime mirror is bug556.

ARIA-031 — STACK_REF_INTO_GC_FIELD

Trigger shape

gc Holder:h = Holder{ p: NULL };
stack int32:x = 7i32;
h.p = @x;                    // ARIA-031 — stack-rooted RHS into gc field

The recogniser walks struct field paths the same way ARIA-029 does. The 3-level case:

gc Holder:h = Holder{ p: NULL };
stack Outer:s = Outer{ mid: Mid{ leaf: Leaf{ value: 7i32 } } };
h.p = @s.mid.leaf.value;     // ARIA-031

This is the contract pinned by bug551.

Where it fires

ARIA-031 fires cleanly from inside if-arm bodies (bug552, REGION-DEC-013). Unlike ARIA-029, the gc-rooted field is allocated outside the arm and there is no leak interaction to shadow the diagnostic. The recogniser sees the field-write at any nesting depth.

What does not trigger ARIA-031

Workaround

Promote-to-gc (bug555) — change the source region from stack to gc. The reference now points into the GC graph and no longer outlives the stack frame:

gc Holder:h = Holder{ p: NULL };
gc Slot:g = Slot{ value: 7i32 };
h.p = @g.value;              // OK

K core 169 (169_nitpick031_promote_workaround_value_flow_pass.npk) locks the runtime value flow of the promote-to-gc pattern via scalar extraction; the C++ runtime mirror is bug557.

The asymmetry, summarised

Property ARIA-029 ARIA-031
Region pair wild ← gc gc ← stack
Fires on bare identifier RHS yes yes
Fires on field-path RHS yes yes
Fires on @arr[N] RHS no (gap) n/a
Fires inside if-arm body no (ARIA-014 shadow) yes
Documented workaround copy-by-value, pin promote-to-gc
K runtime parity test core 168 core 169

Cross-references

Fixture index

Bug What it locks
bug237 ARIA-029 baseline (bare identifier, v0.27.2)
bug238–239 ARIA-029 negative controls (v0.27.2)
bug240 ARIA-031 baseline (bare pin, v0.27.3)
bug241–242 ARIA-031 negative controls (v0.27.3)
bug427–428 ARIA-029 field-path coverage (v0.31.3.2)
bug429–430 ARIA-031 field-path coverage (v0.31.3.2)
bug546 ARIA-029 3-level field path
bug547 deep-field copy-by-value workaround
bug548 deep-field pin escape hatch
bug549 no false positive on unrelated wild alloc
bug550 compose copy + pin
bug551 ARIA-031 deep stack-field path
bug552 ARIA-031 inside if-arm (no shadow)
bug553 no false positive deep gc→gc
bug554 multi-hop gc destination paths
bug555 promote-to-gc workaround
bug556–560 K168/169 mirrors and runtime parity churn