Skip to content

ManifestProvenance cannot say WHY build_git_sha is absent: declined-dirty, never-derived and no-git-dir all collapse to the same omission #100

Description

@iceteaSA

build_provenance (manifest.rs:694) normalizes sentinels and empties to field omission, which is the right call — an omitted field is an honest "no claim" and the sentinel filter stops "unknown" publishing as a fact. But the wire then carries only the fact of absence, never its cause. Three materially different situations produce a byte-identical frame:

situation what it means for a census on the wire
declined-dirty — the build had uncommitted changes, so the stamper refused to name a commit the binary is real but unattributable; someone built from a modified tree build_git_sha absent
never-derived — the release path does not stamp SUBC_BUILD_GIT_SHA at all the module has no provenance pipeline; nothing is wrong with this build build_git_sha absent
no-git-dir — built from a tarball/vendored source with no repository attribution is impossible by construction, not by policy build_git_sha absent

The first is a warning about this artifact. The second is a gap in a repo's release machinery. The third is a permanent property of how it was built. A fleet census reading build_git_sha: absent cannot tell them apart, so the only available response is the union of all three — and in practice that means the signal gets ignored.

Note what is already distinguishable and does not need fixing: block-absent vs field-absent. build_provenance returns the block unconditionally (wire_crate_version is always knowable from SUBC_PROTOCOL_CRATE_VERSION), so "made no claim at all" and "claimed, but cannot name a commit" are already different frames. Cerebellum corrected me on exactly this when I first drafted the finding as smuggling-prevention — that framing argues for something the wire already has. The gap is narrower and real: absence has no cause.

Ask

An optional reason beside the fact, wire-additive in the established shape (Option, skip_serializing_if), costing old readers nothing:

/// Why `build_git_sha` is absent. Present only when the field is absent;
/// a stamper that names a commit says nothing here.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_git_sha_absence: Option<ShaAbsence>,   // DeclinedDirty | NeverDerived | NoGitDir

Two riders I'd want in the implementation rather than left to callers:

  1. The absence reason must be derived by the same helper that decides the omission. If callers pass it in alongside the sha, the two can disagree — a module could declare NeverDerived while passing a sha, or claim DeclinedDirty on a clean tree. build_provenance already owns the normalize-and-validate decision; it should own the reason too, which means taking the tree state as an input rather than a pre-computed verdict.

  2. The stamp rule should take its inputs as parameters, not read env! inline. This is Cerebellum's point and it is the sharper half of the issue. A stamp that reads env!("SUBC_BUILD_GIT_SHA") inline has a refusing branch that has never executed — you cannot produce a dirty build inside a test without rebuilding, so the branch exists only in the author's intention. A parameterized attestable_commit(rev, tree_state) is testable at clean/dirty/empty/every-sentinel. Whatever shape the absence reason takes, it should be reachable in a test at every value it can hold; otherwise this issue adds a second never-executed branch next to the first.

Ordering

This costs every ManifestProvenance constructor a line, so it wants to land after the constructor work in #84 — which it now does. I raised that dependency when #84 was open as an argument for #84; it is satisfied at 856e26f7.

Related: #83 (helper moved into subc-protocol so transport-direct modules can reach it), #87 (canonical 40/64 hex enforced at construction — a second case of the helper constraining what prose could not). Both are the same pattern this extends: put the decision in the constructor, where it cannot be copied wrong.

Happy to take it. If you'd rather see the absence reason as a bare string than an enum, say so — I'd argue for the enum with a #[serde(other)]-style fallback per #79's ruling on nested diagnostic value enums, since it is exactly that class.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions