MVF-2026-0004 — an audit entry with no sequence number ended the walk as though intact
The audit walk carried "did the chain break" and "where did it break" in a single number, where zero meant no break.
| Identifier | MVF-2026-0004 | | Internal row | M26-CUS-006 | | Severity | S2, priority P0 | | Affected | no released version | | Not affected | every release: 1.0.0, 1.0.1, 2.0.0, 2.0.1, 2.1.0, 2.2.0, 2.3.0, 2.4.0 and 2.5.0 | | Fixed in | 2.6.0 | | Reported | internally; Stage B1 audit, confirmed 2026-09-24 | | Published | 2026-10-05 | | CVE / GHSA | none assigned | | CVSS:3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N | | CVSS:4.0 | CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N |
Summary#
The audit walk carried "did the chain break" and "where did it break" in a single number, where zero meant no break. It read the breaking entry's sequence number with a helper that answers zero for a key that is missing, for a value that is not a number, and for one that will not fit an integer. So an entry whose sequence number was absent, zero, a string or fractional set the break marker to zero at the moment the walk broke on it, and the result then said the links were intact and overwrote the diagnostic that had just been written. An entry appended after the last genuine one verified as an intact chain, and because the walk stopped before updating the running hash, the computed head stayed the genuine one — so the comparison against the head sealed into the case manifest passed as well.
Why this page exists although no release was affected#
Every identifier in this register has a page, so a citation never lands on a gap.
For an operator: there is nothing to do. No released version contains
audit_verify.
Affected range, checked at each tag#
builtin/audit.go was introduced after the v2.5.0 tag and is absent at all nine
tags. The defect was live on dev for 24 commits, between 2026-09-20 and the fix
on 2026-09-25. That window is the honest statement of exposure; "no one was ever
exposed" is true only of published releases.
Details#
One int64, firstBroken, was both the flag and the position. The sequence
number came from a general-purpose reader that returns zero rather than an error
for a missing or ill-typed value, so a forged entry needed no knowledge of the
chain at all: even a deliberately wrong predecessor link merely became the break
that was then misreported as no break.
The walk stops before advancing the running hash, so for an entry appended after the last retained one the computed head remained the hash of the last genuine entry. Both the head comparison and the anchor comparison therefore agreed with the sealed manifest. That is a fully silent pass.
Which half the anchor caught, and which it did not#
This distinction is the reason the page is worth reading, and compressing it would overstate the defect.
| the tamper | links reported intact | head and anchor |
|---|---|---|
| an entry appended after the last genuine one | yes, wrongly | also passed — a silent pass |
| a middle entry edited with its sequence number deleted | yes, wrongly | failed — only the anchor caught it |
So "a doctored audit log verified as intact and anchored" is true of the append case and not of the mid-edit case. The internal verification note records exactly that: for the second variant, only the anchor comparison catches it.
The fix#
The single counter is split into three: a boolean for whether it broke, a
1-based index for where in the log, and the sequence number it broke at, which
is zero when the entry had none. The intactness flag now reads the boolean. A new
auditEntrySeq accepts only a positive integer, so an entry without one is
itself the break rather than a silent zero. A consecutiveness check and a genesis
check for the first link were added, and a broken_index field is published so a
caller is told where.
The same commit also stopped the verifier copying its entry, dropped and
completeness numbers out of the document being verified. That half is
MVF-2026-0003, a different defect with its own fixture.
What this does not mean#
audit_verify still does not detect tampering on its own, and is not claimed
to. builtin/audit_test.go contains
TestARewrittenChainPassesItselfAndFailsItsAnchor precisely because a chain
rebuilt wholesale from the genesis passes every internal check and is caught only
by its anchor. The document's own standing limitation is unchanged and still
printed with the result: no document can show that a document is missing.
Fixture#
builtin/audit_test.go, TestAnEntryWithoutAPositiveSeqBreaksTheChain.
Workarounds#
None is needed: no published release contains audit_verify.
For a development build in the affected window, there was a reliable detector for this defect, out of band: count the elements of the document's own log array in the file and compare that count with the number of links the result says it checked. On an intact log the walk reaches every entry, so a result claiming the links are intact while having checked fewer links than there are entries is this defect's exact signature, and it catches both verified variants.
What it does not cover, precisely: the operator must count the array themselves,
because before the fix the result's own entry count was copied out of the
document and so was chosen by whoever tampered with it; it does nothing for
MVF-2026-0003, the front-truncation case, where the link
count does equal the number of entries and the anchored head still matches, so it
must never be read as making the pre-fix verifier trustworthy overall; it is a
manual comparison rather than a verdict any script receives; and it cannot see a
log deleted or truncated as a whole, or a chain rebuilt consistently from the
genesis — neither can the fixed verifier.
Both variants require write access to the written log file. The in-memory chain is not reachable this way, because the writer always records a positive sequence number. Keeping the written log out of a tamperer's reach — append-only or write-once storage, or recording its digest out of band at write time — prevents the forgery, but that is a storage control rather than a fix, and it changes nothing about the verdict the pre-fix verifier returns on a file that was already tampered with.
Credit#
Found by this project's own 2.6.0 security review, Stage B1, on 2026-09-24, and confirmed in Stage B4 the same day. No external reporter.
References#
- The entry in CHANGELOG.md under 2.6.0, Security, which
describes this and
MVF-2026-0003together. - SECURITY.md for the severity scale.
- The advisory register.