Skip to content

ltm: unguarded slot as u16 cast on the SiteId slot axis can alias occurrences for a >65,535-element arrayed target #979

Description

@bpowers

LTM occurrence identity is a SiteId: a path of u16 components. The FIRST component is the slot index -- for an Ast::Arrayed target, its per-element equation slots numbered in canonical element-key-sorted order (the optional default equation is the slot after the last element).

The builtin-arity axis of that path was hardened on branch roundtrips-track-a (commits b0a2a6f0, d5eed63b): db::ltm_ir::site_child_index returns Option<u16> and declines out-of-range indices, a reserved UNADDRESSABLE_CHILD (= u16::MAX, src/simlin-engine/src/db/ltm_ir.rs:615) sentinel is never emitted by the producer, and ltm_augment::child_path maps an over-arity child to that sentinel with a CHECKED conversion -- so a lookup at or below an unaddressable child provably misses instead of aliasing an earlier sibling. That design exists precisely because a wrapped index re-uses another occurrence's SiteId, and the ceteris-paribus wrap then holds or freezes the WRONG reference and emits a plausible but wrong link score (silent corruption).

The slot axis did not get the same treatment. These casts are unchecked (line numbers verified against roundtrips-track-a @ 86df68bf):

Consumer side, src/simlin-engine/src/ltm_augment.rs, in build_arrayed_link_score_equation (declared at line 3575):

  • line 3682 -- per enumerated element slot:
    slot_equation(expr, gf_table_ref.as_deref(), slot as u16)?,
  • line 3693 -- the default slot:
    sorted_slots.len() as u16,

Producer side mirrors it, src/simlin-engine/src/db/ltm_ir.rs, in the Ast::Arrayed arm of collect_all_reference_sites_and_occurrences:

  • line 501 -- acc.path.push(slot as u16); (per-element slots)
  • line 517 -- acc.path.push(elem_keys.len() as u16); (default slot)

For an arrayed target with more than 65,535 element equations, slot as u16 wraps and two distinct slots collapse onto the same SiteId prefix -- the same aliasing class the builtin-arity axis now refuses.

Note that #978's framing is inaccurate on this point: it states "Every AST-shaped child count fits a u16 by construction, EXCEPT builtin arity." The arrayed slot count is the counterexample. The other unchecked as u16 pushes in walk_all_in_expr really are bounded by construction and are NOT part of this issue: ltm_ir.rs:809 (i as u16 over subscript indices, bounded by the reference's declared subscript arity) and ltm_ir.rs:976 (child as u16 over the fixed three Expr2::If children).

Why it matters

  • The consequence is silent. A wrapped slot index yields a wrong-but-plausible link score, not a crash, a panic, or a diagnostic. There is no loud failure mode to notice in the field.
  • The asymmetry is a trap for the next reader. One axis of the same SiteId path is now explicitly guarded with a reserved sentinel and rustdoc explaining the aliasing hazard; the sibling axis is a bare as. Someone comparing the two could reasonably assume both were handled -- and ltm: consider rejecting over-arity builtins at the LTM front door to delete the SiteId unaddressable-child machinery #978's body currently encourages that assumption.

Components affected

  • src/simlin-engine/src/db/ltm_ir.rs (producer: occurrence SiteId construction)
  • src/simlin-engine/src/ltm_augment.rs (consumer: build_arrayed_link_score_equation, plus the checked child_path precedent)
  • src/simlin-engine/src/ltm_augment_occurrence.rs -- on roundtrips-track-a the occurrence-lookup helpers (SlotOccurrences, OccurrenceLookup) were extracted here, so the checked-conversion precedent (child_path, child_is_addressable) lives in ltm_augment.rs while the slot-keyed lookup types live in the new file.

Severity / reachability

Latent, not reachable in practice today. Per #977, LTM link-score generation is quadratic in an arrayed target's element count -- (N+1)^2 generated equation arms per edge, measured at ~2.3s of generation for N=400 -- so a 65,536-element arrayed target is many orders of magnitude past where LTM generation completes at all. This is filed for the silent-consequence and reader-trap reasons above, not because anyone can trip it.

Suggested direction

Either:

  1. Apply the same treatment as the arity axis -- a checked conversion plus the reserved-sentinel / decline path; or
  2. If a slot bound is easier to establish than a per-child one, reject an arrayed target with more than u16::MAX element equations at the LTM front door.

#978 tracks a related simplification (rejecting over-arity builtins at the front door so the sentinel machinery can be deleted entirely). If #978 is taken, the slot axis should be folded into the same front-door check rather than growing a second, parallel mechanism. Whichever path is chosen, #978's "EXCEPT builtin arity" claim should be corrected in the same change.

Cross-references

How it was discovered

Noticed while reviewing the over-arity SiteId guard work on branch roundtrips-track-a: the guard covers the builtin-arity axis of the path but not the slot axis. Out of scope for that branch; filed so the known aliasing cast is not documented solely in a chat report.

Metadata

Metadata

Assignees

No one assigned

    Labels

    engineIssues with the rust-based simulation engineltmLoops that Matter (LTM) analysis subsystemtech debt

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions