Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

The native-module ABI and reclamation scheme each have three live, unreconciled answers

Источник: dev/fera-research/docs/oc1/010-native-abi-and-reclamation-three-answers.md (read-only; сгенерировано scripts/sync-docs.mjs)

The native-module ABI and reclamation scheme each have three live, unreconciled answers

Section titled “The native-module ABI and reclamation scheme each have three live, unreconciled answers”
  • kind: problem

  • area: modules

  • evidence — reclamation, three answers:

    Source Answer
    ADR.md:812-813 (ADR-020 addendum, Accepted) “Relink on hot-swap uses ERB (epoch-based reclamation) — new pointer table in a new epoch, old epoch drains before free”
    OQ.md:868-871 (“final synthesis”, OQ.md:847 claims “all contradictions resolved”) “Inter-module pointer tables = QSBR with quiescent points placed in the actor loops (our known safe points, not arbitrary Tokio tasks). Big-module unloads = Hazard Pointers… No guard ever held across .await. Timeout + HP fallback if a grace period stalls.”
    OPTIM-IDEAS-4-oq24.md:151-157 EBR/epoch for “короткие lock-free host tables”, QSBR only for “контролируемый dedicated reader loop” — i.e. silently drops QSBR from the module-table role and picks a third answer
  • native ABI, three answers:

    Source Answer
    ADR.md:784-786 “Flat, simple, hot-path types are #[repr(C)] structs passed by value across the ABI” ✔
    ADR.md:807-809 “modules export tables of pointers (static EXPORT: &VTable); the host resolves and patches importer slots at link time… a virtual-function call without vtable walking” — a vtable, and &VTable is a reference into the module’s data
    OPTIM-IDEAS-4-oq24.md:89-90 “Нельзя строить долгоживущий ABI на extern "Rust", Rust trait objects, String, Vec, references, closures, Rust vtables… нужен узкий версионированный C ABI: extern "C", фиксированные integer types, #[repr(C)] POD-структуры, opaque handles и явные create/destroy в одном владельце памяти”
    OQ.md:878-880 “(d) pin the Rust compiler version so our own modules compile without #[repr(C)] — a fixed rustc version makes the Rust ABI stable between our modules. Manifest consequence: module metadata must carry the required rustc version; the host checks it at load.”

    OQ.md:878-880 and OPTIM-IDEAS-4:89 are mutually exclusive for first-party native modules: one mandates #[repr(C)] POD + opaque handles, the other removes the need for #[repr(C)] entirely. And OQ.md:878-880’s “manifest must carry the required rustc version” is a new manifest field with no ADR-021 amendment and no QA gate.

  • OQ.md:847 — “FINAL FORMULATION (2026-10-02, all contradictions resolved)” — the contradictions are not resolved.

  • QA.md:129-132 tests neither ABI rule nor reclamation rule.

  • Acronym typo “ERB” for EBR at ADR.md:812, OQ.md:639, OQ.md:741.

  • ADR-020’s own dependency-graph duty is untouched by any of this: ADR.md:795-796 — “manifest layering and the dependency graph become load-bearing metadata (load-order correctness)”.

  • severity: high

The module boundary is the spine of this architecture — ADR-001 makes every heavy subsystem swappable through it, ADR-020 sets its granularity, ADR-021 versions it, ADR-024 distributes and hot-swaps it. Yet the two most mechanical questions about it (what exactly crosses the ABI, and what reclaims it) have three incompatible answers each, and the file that claims to have closed them (OQ-024, marked “design settled / all contradictions resolved”) is itself on one side of both contradictions.

Why each position is not merely cosmetic:

  • #[repr(C)] vs pinned-rustc. With #[repr(C)] POD + opaque handles, a module built by any rustc is loadable, the ABI is stable forever, and the host validates it structurally (abi_version, struct sizes — exactly what ADR.md:830-831 says abi_version is for). With a pinned rustc, stability comes from the compiler, and every module now carries a rustc requirement the host must check and the manifest must carry (OQ.md:880-881). That also quietly moves safety from structural validation to provenance — a worse position for a trust model whose entire threat story is module supply chain (ADR.md:100-114). And a pinned-rustc ABI is exactly the thing nylon-ring’s “explicit version + struct-size validation on load” (ADR.md:19-20) was chosen to avoid depending on.

    Additional defect in the pinned-rustc option, surfaced by oc5 (bus, seq 22) and independently consistent with finding 021: the pin is enforced by a self-reported string. “the host checks it at load” (OQ.md:880-881) compares a manifest field saying “built with rustc X” against the host’s own version — nothing proves the dylib was produced by rustc X. Combined with ADR-004’s non-blocking hash warning (ADR.md:104-107) and QA.md:121-124’s mandated test that loading proceeds anyway, a hand-built or tampered module that merely claims the right rustc passes. So the mechanism as specified does not deliver the guarantee the corpus asserts for it (“a fixed rustc version makes the Rust ABI stable between our modules”). Fix, if the option is chosen: verify the artifact’s own embedded compiler metadata (e.g. the .comment/producer section or a build-id binding) rather than the manifest’s claim — which is one more reason the structural option (A) is the safer default.

  • vtables vs pointer tables. ADR.md:807 static EXPORT: &VTable is a Rust reference to a vtable — i.e. exactly the constructs OPTIM-4:89 forbids across a long-lived ABI (“references… Rust vtables”). OQ.md:875-876 (b) “remove virtual tables in favor of link-time pointer tables” is the same disagreement from the other direction. ADR.md:807 is dated 2026-09-30; OQ.md:874-883 is dated 2026-10-01/02; OPTIM-4 is the synthesis — so ADR-020 is the stale one, but nobody amended it.

  • EBR vs QSBR vs split. They differ in where the quiescent point goes: EBR needs no participant cooperation (expensive epochs), QSBR needs participants to announce quiescence (cheap epochs, needs the actor loops instrumented — OQ.md:868-869 says exactly that), HP is precise but O(objects). Since ADR.md:807-813 puts pointer tables in the host and the actor loops are ours (ADR.md:152-156), QSBR is the natural fit and EBR the safe default; but the choice determines whether our actor loops must be instrumented, which is a real code-shape commitment nobody has made explicitly. The three answers also disagree on whether “no guard across .await” (OQ.md:870) is a rule or an aspiration.

There is a fourth, quieter problem: the reclamation choice interacts with my finding 009 (EBR/QSBR do not make dlclose safe). Whichever scheme wins, the code-page policy is separate and currently unwritten — so “EBR” in ADR-020 reads as if it covers unload safety, and it does not.

Варианты решения (tradeoffs)

Section titled “Варианты решения (tradeoffs)”
  • (A) #[repr(C)] POD + opaque handles, C ABI, no vtables, QSBR for module pointer tables with quiescent points in the actor loops. Consistent with ADR.md:784 (kept), supersedes ADR.md:807’s &VTable, and satisfies OPTIM-4:89. Costs: instrumenting the actor loops for quiescent points (OQ.md:868); more boilerplate at the boundary; the fork plan in OQ.md:878-880 (item (d)) is dropped.
  • (B) Pinned-rustc Rust ABI (OQ.md:878-880). Cheapest calls (no marshaling at all), but stability is compiler-provenance-based, every module needs a rustc requirement in the manifest, and third-party native modules (dev mode, ADR.md:442-446) would need the same pin — a much stronger requirement to impose on strangers. Also directly contradicts nylon-ring’s raison d’être.
  • (C) #[repr(C)] POD + pinned rustc as belt-and-braces. Both constraints hold; costs the ergonomics of (B) with none of its benefit.
  • (D) EBR everywhere (keep ADR.md:812). Simplest conceptually, no actor-loop instrumentation; higher steady-state cost on hot paths and it still does not solve dlclose (finding 009).

(A). It is the only option that keeps abi_version meaningful (ADR.md:830-831), keeps third-party native modules plausible, and matches the one document in the corpus that engaged with the actual unsafe-code mechanics.

  1. ADR-020 addendum superseding ADR.md:807-809: export a #[repr(C)] flat pointer table (not &VTable), opaque handles for anything owning memory, explicit create/destroy ownership; per OPTIM-4:89-90.
  2. ADR-020 addendum fixing reclamation to QSBR with quiescent points in the actor loops + HP for large-module unload + the “no guard across .await” rule as normative (OQ.md:868-870), and explicitly deferring the code-page question to finding 009’s policy.
  3. OQ-024 amendment removing item (d) (pin rustc / no #[repr(C)]) and the resulting manifest field, or — if the author actually wants (B) — make that an explicit ADR with the third-party-consequence argument written down. Right now the choice is implied in one OQ bullet and contradicted by the file written to implement it.
  4. Add the two missing QA gates: (a) every symbol crossing the native ABI is #[repr(C)]/opaque-handle — checkable with a trybuild/lint gate over the contract crate; (b) quiescent-point instrumentation is present in every actor loop that can hold a module pointer.
  5. Mechanical: fix ERB → EBR at ADR.md:812, OQ.md:639, OQ.md:741.