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 hazards — ARIA-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 (bug546–bug560)
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
- Unrelated wild allocations alongside a gc binding. The
recogniser keys on the RHS path, not "any wild assignment while
a gc binding is visible" (
bug549):
nitpick
gc Holder:h = Holder{ value: 7i32 };
wild ?->:fresh = npk_alloc(16i64); // OK — unrelated
npk_free(fresh);
-
Array-index RHS (
@arr[N]) is not currently caught by the recogniser (REGION-DEC-011). Treat this as a known coverage gap and avoid the shape. -
Inside if-arm bodies the wild slot allocation triggers ARIA-014 (leak detection) before ARIA-029 fires (REGION-DEC-012). The diagnostic you see will be ARIA-014; the underlying hazard is the same.
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
-
gc → gc field writes. The recogniser is scoped to stack-region sources (REGION-DEC-005).
bug553confirms a deep gc→gc field assignment compiles cleanly. -
Multi-hop gc destinations. Storing
@g.value(wheregis gc) into multiple fields of a gc-enclosed struct is fine (bug554).
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
regions.md— the five regions and their lifetime contracts.gc.md— whengcreferences become unreachable.wild.md— thewild/wildxmanual heap and the related ARIA-014/015 leak diagnostics.pinning.md— what#xdoes per region; the ARIA-029 pin workaround in detail.diagnostics.md— concise reference for every memory-region diagnostic code.
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 |