Nothing couples emission of TAG_SYMBOL_BASENAME to the fact_minor_min declaration that is supposed to bracket it #280

Open
opened 2026-09-16 09:24:04 +02:00 by buildagent · 0 comments
Member

Split out of #277, whose disclosure fix landed in d5547cc. This half was NOT fixed there — it has a different fix location and deserves its own decision.

The claim

crates/abi/src/frame.rs argues that TAG_SYMBOL_BASENAME (0x8004) is the one optional tag that cannot degrade safely, because the field it annotates — a symbol's name — has no absent encoding. An older host skipping it does not thin the answer; it stores the guest's PLACEHOLDER as if it were a name. The doc's word is that it MISLEADS, which is worse than degrading and worse than refusing.

_prdoc/guides/80-threat-model.md §4.2c states the mitigation as a property of the system:

A package emitting it declares fact_minor_min = 2, and a host below that refuses the package at install with manifest.abi_unsupported rather than storing the placeholder.

It is a property of a COOPERATING package

Manifest::validate refuses fact_minor_min > ABI_MINOR — a package cannot ask for a host newer than the one it meets. Nothing anywhere ties EMISSION of the tag to the DECLARATION.

A third-party package that calls Encoder::symbol_basename while declaring [abi] fact_minor_min = 0 installs happily on a host at ABI_MINOR = 1, whose decoder takes the t if t >= TAG_OPTIONAL_BASE => continue arm and stores the placeholder verbatim as a symbol name. That is precisely the case the bracket is documented as preventing.

Nothing in the tree grades this. Dashboard.expected says so itself: "The expectation format has no field for the annotation itself (ExpectSymbol is deny_unknown_fields), so this table cannot assert that the tag was attached." The tag's presence is graded only by the daemon e2e; the ABI bracket is graded nowhere.

Scope

No shipped package is affected. de.h-dv.svelte declares fact_minor_min = 2 correctly, and it is the only emitter. This is a trap for a well-meaning third-party author, and — more importantly — a sentence in the threat model that reads as a system property when it is a convention.

The related finding from the same review: crates/guest/src/frame.rs's ABI_MINOR is 0, so a guest stamps minor 0 into every frame it writes while those frames may carry a minor-2 record. frame.rs's "an older parent meeting a newer worker is the refused case" therefore never fires for this tag, because the worker never declares itself newer. wire_parity.rs records the non-move as deliberate (moving it moves PINNED_REV), and 09305a3 shows that pin does get moved when needed — so "it is expensive" is not the whole answer.

Where the fix belongs

plugin check's conformance leg. It is the one place that holds BOTH the decoded ValidatedFacts — hence Symbol::name_is_basename — and the Manifest. A package whose frame carries the tag while its manifest declares fact_minor_min < 2 should fail conformance, which means it can never reach plugin enable (which requires the verdict).

Two alternatives worth weighing rather than assuming:

  1. Refuse at validate. Cleanest in principle — the decoder knows the tag arrived. It does not know the manifest: abi::validate takes (body, src, limits, grants) and has no manifest by design, for the same reason it refuses a received line. Handing it one would be the wrong shape.
  2. Refuse at plugin pack. Catches it at authoring time, which is friendlier. But packing does not decode a frame, so it would have to run the extractor — which is what plugin check is for.

(1) is the tempting one and (2) is the friendly one; the conformance leg is probably right because it already executes the package and already gates enable.

Also from #277, and NOT a defect — a decision someone should make

Should GrantSpec::Requested grant derived_names at all by default? plugin add with no --grant resolves it to whatever the manifest asked for. After d5547cc the confirmation screen NAMES it, so the reported hole is closed. Every other high-authority answer in this tree is opt-in, so "everything the manifest asked for, including this" is now defensible but no longer obviously right. Changing it is a behaviour change for anyone scripting plugin add, which is why it was not bundled into the disclosure fix.

Split out of #277, whose disclosure fix landed in `d5547cc`. This half was NOT fixed there — it has a different fix location and deserves its own decision. ## The claim `crates/abi/src/frame.rs` argues that `TAG_SYMBOL_BASENAME` (0x8004) is the one optional tag that cannot degrade safely, because the field it annotates — a symbol's `name` — has no absent encoding. An older host skipping it does not thin the answer; it stores the guest's PLACEHOLDER as if it were a name. The doc's word is that it **MISLEADS**, which is worse than degrading and worse than refusing. `_prdoc/guides/80-threat-model.md` §4.2c states the mitigation as a property of the system: > A package emitting it declares `fact_minor_min = 2`, and a host below that refuses the package at install with `manifest.abi_unsupported` rather than storing the placeholder. ## It is a property of a COOPERATING package `Manifest::validate` refuses `fact_minor_min > ABI_MINOR` — a package cannot ask for a host newer than the one it meets. **Nothing anywhere ties EMISSION of the tag to the DECLARATION.** A third-party package that calls `Encoder::symbol_basename` while declaring `[abi] fact_minor_min = 0` installs happily on a host at `ABI_MINOR = 1`, whose decoder takes the `t if t >= TAG_OPTIONAL_BASE => continue` arm and stores the placeholder verbatim as a symbol name. That is precisely the case the bracket is documented as preventing. Nothing in the tree grades this. `Dashboard.expected` says so itself: *"The expectation format has no field for the annotation itself (`ExpectSymbol` is `deny_unknown_fields`), so this table cannot assert that the tag was attached."* The tag's presence is graded only by the daemon e2e; the ABI bracket is graded nowhere. ## Scope **No shipped package is affected.** `de.h-dv.svelte` declares `fact_minor_min = 2` correctly, and it is the only emitter. This is a trap for a well-meaning third-party author, and — more importantly — a sentence in the threat model that reads as a system property when it is a convention. The related finding from the same review: `crates/guest/src/frame.rs`'s `ABI_MINOR` is **0**, so a guest stamps minor 0 into every frame it writes while those frames may carry a minor-2 record. `frame.rs`'s "an older parent meeting a newer worker is the refused case" therefore never fires for this tag, because the worker never declares itself newer. `wire_parity.rs` records the non-move as deliberate (moving it moves `PINNED_REV`), and `09305a3` shows that pin does get moved when needed — so "it is expensive" is not the whole answer. ## Where the fix belongs **`plugin check`'s conformance leg.** It is the one place that holds BOTH the decoded `ValidatedFacts` — hence `Symbol::name_is_basename` — and the `Manifest`. A package whose frame carries the tag while its manifest declares `fact_minor_min < 2` should fail conformance, which means it can never reach `plugin enable` (which requires the verdict). Two alternatives worth weighing rather than assuming: 1. **Refuse at `validate`.** Cleanest in principle — the decoder knows the tag arrived. It does not know the manifest: `abi::validate` takes `(body, src, limits, grants)` and has no manifest by design, for the same reason it refuses a received line. Handing it one would be the wrong shape. 2. **Refuse at `plugin pack`.** Catches it at authoring time, which is friendlier. But packing does not decode a frame, so it would have to run the extractor — which is what `plugin check` is for. (1) is the tempting one and (2) is the friendly one; the conformance leg is probably right because it already executes the package and already gates `enable`. ## Also from #277, and NOT a defect — a decision someone should make Should `GrantSpec::Requested` grant `derived_names` at all by default? `plugin add` with no `--grant` resolves it to whatever the manifest asked for. After `d5547cc` the confirmation screen NAMES it, so the reported hole is closed. Every other high-authority answer in this tree is opt-in, so "everything the manifest asked for, including this" is now defensible but no longer obviously right. Changing it is a behaviour change for anyone scripting `plugin add`, which is why it was not bundled into the disclosure fix.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#280
No description provided.