| Document | draft-fossil-artifact-format-00 |
| Section | 4.1.2 — Sterilization |
| Type | Technical (three factual divergences from the reference implementation) |
| Status | Draft — not yet applied |
| Reported | 2026-08-08 |
| Submitter | Dan Mestas |
| Found via | zit import-git mirror classifier, against a 36,383-commit SQLite Git mirror |
1. Summary
§4.1.2 describes Fossil's sterilization marker — the comment line a writer adds to make content deliberately unparseable as a structural artifact. Three of its statements are wrong against the reference implementation: the marker's position, the gate that rejects the sterilized bytes, and the marker's literal text. The section is additionally self-contradictory: it says the marker is the first line, then says the signed form places it before the trailing signature block, which is after the cards.
The direction of the error is the dangerous one. An implementer following §4.1.2 as written builds a sterilizer that does not match Fossil's, and the mismatch is invisible until a sterilized artifact reaches another repository — where it surfaces as a foreign check-in rather than as a parse failure.
2. Location
§4.1.2 (Sterilization), the paragraph beginning "This subsection is informative." Current text:
To render content deliberately unparseable as a structural artifact, a writer inserts a comment line beginning with # (of the form
# Remove this line to create a well-formed Fossil artifact.) as the first line, which causes the line-1 gate of Section 4.1 to fail. For PGP- or SSH-signed content, the marker line is instead spliced in immediately before the signature block…
3. The defect
Verified against Fossil 2.28 (52445a27f1) by source reading and by running the
code.
3.1 Position — the marker is appended, never inserted first
sterilize_manifest() (src/manifest.c:2238) builds its output with
blob_appendf. The marker lands after the final card, not before the first.
Observed on a real checkout:
U tester
Z 633267631377a908d4e09def80ef9fd5
# Remove this line to create a well-formed Fossil manifest.
3.2 Failure mechanism — residue, not the line-1 gate
Because the marker is trailing, it never reaches the line-1 gate. It is rejected by §4.1's rule that any bytes left unconsumed after the card loop MUST cause rejection — equivalently §4.2.1's residual-bytes clause. A reader that implemented only the stated line-1 mechanism would accept a sterilized artifact, which is the precise outcome sterilization exists to prevent.
3.3 Literal text — the quoted string is never emitted
The format string (src/manifest.c:2242) is:
"# Remove this line to create a well-formed Fossil %s.\n"
%s is manifest for CFTYPE_MANIFEST and control artifact for every other
type. The draft's …well-formed Fossil artifact. is emitted by no code path.
3.4 Internal contradiction
"as the first line" and "spliced in immediately before the signature block" cannot both hold: the signature block is trailing, so a marker before it is after the cards. The signed description is the accurate one, and it generalizes — the unsigned case is the same insertion point with an empty signature.
4. Impact
Two mechanisms, in opposite directions.
A reader built to §4.1.2 under-rejects. It looks for a line-1 marker, finds none, and parses a sterilized artifact as a structural artifact.
A writer built to §4.1.2 diverges byte-for-byte from Fossil, so its output is not the artifact any Fossil-derived name refers to.
This is not theoretical, and the consumer-side cost is measurable. zit's Git
import proves a mirror's manifest/manifest.uuid pair by stripping the
sterilizer and checking the result against the name the mirror recorded. Written
to the trailing-only reading of the implementation — which is correct for the
unsigned case — it failed for every PGP-clearsigned check-in, because there the
marker sits mid-blob, immediately before -----BEGIN PGP SIGNATURE-----. On a
36,383-commit SQLite mirror:
| Proof outcome | Count |
|---|---|
| Proven by trailing strip | 35,307 |
| Proven only once the marker is spliced from before the signature block | 1,076 |
| Unproven | 0 |
All 1,076 date from 2009–2011, when SQLite's clearsign setting was on. Each
had its mirror metadata retained permanently — the observable symptom of a
correct implementation built against an incorrect description of where the
marker goes.
5. Correction applied
Replace the first paragraph of §4.1.2 with text that states the position once, in the general (signature-aware) form, names the correct rejection mechanism, and quotes the two literals the implementation actually emits:
- This subsection is informative. To render content deliberately
- unparseable as a structural artifact, a writer inserts a comment line
- beginning with # (of the form # Remove this line to create a well-
- formed Fossil artifact.) as the first line, which causes the line-1
- gate of Section 4.1 to fail. For PGP- or SSH-signed content, the
- marker line is instead spliced in immediately before the signature
- block, with the boundary recomputed by the same wrapper-detection
- logic of Section 4.1.1, so the signature remains trailing.
+ This subsection is informative. To render content deliberately
+ unparseable as a structural artifact, a writer appends a comment line
+ beginning with # after the final card, and before the signature block
+ when the content is PGP- or SSH-signed, so that a signature remains
+ trailing; the boundary is located by the same wrapper-detection logic
+ of Section 4.1.1. The marker reads # Remove this line to create a
+ well-formed Fossil manifest. for a check-in and # Remove this line to
+ create a well-formed Fossil control artifact. for any other type.
+ Because the marker follows the cards, it is Section 4.1's rule that
+ unconsumed bytes after the card loop MUST cause rejection —
+ equivalently the residual-bytes clause of Section 4.2.1 — that rejects
+ the artifact, not the line-1 gate.
6. Rationale
- One statement of position. The contradiction exists because position is asserted twice. Stating it once, in the form that covers both cases, removes it.
- The mechanism is load-bearing. Naming the wrong gate produces a reader that fails open on exactly the input sterilization targets. This is the most consequential of the three defects.
- The literals are quoted because they are compared. Consumers match the marker to strip it; a consumer matching the draft's string matches nothing Fossil writes.
- Informative, not normative. §4.1.2 does not impose requirements — the correction changes only what the section describes, not what any reader or writer is required to do.
Note on the producer side
Fossil's own use of sterilize_manifest() narrowed in 722c248d53 (shipped
2.18, 2022-02-23), which removed it from tar.c and zip.c, so archive exports
are no longer sterilized while checkouts and Git export still are. That is a
producer-policy change and needs no draft text; it is recorded here because it
makes §4.1.2's accuracy matter more, not less — consumers can no longer assume
content arriving from a Fossil-derived tarball has been sterilized at all.
Prepared against the frozen final draft-fossil-artifact-format-00. The zit
reference implementation already emits and strips the marker at the position
described in §5; this erratum records the divergence so the draft can settle it.
7. Resolution verification
- All three defects verified twice: by reading Fossil 2.28 source
(
src/manifest.c:2238,:2242) and by observing a live checkout'smanifestfile and a published SQLite mirror's blobs. - The 1,076/35,307/0 split is reproducible: export
sqlite.fossilwithfossil git export, then for each commit comparemanifeststripped both ways againstmanifest.uuid. - Clean-room effect: an implementer no longer has to discover, from a foreign check-in appearing in an unrelated repository, that the marker is trailing rather than leading and that the text varies by artifact type.