MVF-2026-0003 — an audit log with its first entries deleted verified as intact
auditverify could be handed a written audit log with a run of entries deleted from the front and report it as intact, anchored and complete.
| Identifier | MVF-2026-0003 | | Internal row | M26-CUS-005 | | 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#
audit_verify could be handed a written audit log with a run of entries deleted
from the front and report it as intact, anchored and complete. Every
surviving entry still carried the correct link to its predecessor and the
recomputed head still matched the head sealed into the case manifest, because
deleting a leading run breaks no link that the walk examined. Three of the
numbers in the result — the entry count, the dropped count and whether the chain
was complete — were copied out of the document being verified rather than
computed from its entries, so a tamperer could also re-describe the shortened
log as a legitimately capped one.
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 the answer is short: there is nothing to do. The audit chain
and audit_verify are not in any released version of Mutant, so no installed
copy has this defect and no audit log written by a release can carry it.
Affected range, checked at each tag#
builtin/audit.go and security/audit_sink.go were both created after the
v2.5.0 tag. At every one of the nine tags the subject file is absent, the subject
functions are absent, and no part of the audit chain feature exists. The defect
was live on dev between 2026-09-20 and the fix on 2026-09-25.
Details#
AuditVerify decoded the written document, handed its log array to
auditWalk, and then overwrote three of the walk's own answers with values
read straight back out of that same document: the entry count, the dropped count
and the chain-complete flag.
auditWalk began with no previous hash and compared an entry's recorded
predecessor only once it had one, so the first retained entry's link was accepted
unexamined. Its sequence number was never compared with the genesis, with the
next entry's, or with the document's own first-retained-sequence field, which the
writer emits and the walk ignored.
The head of the chain is the hash of the last entry, and every surviving entry still carries its own correct link and hash. So deleting a contiguous run from the front left every recomputed link and the computed head bit-identical: links intact, head matches, and the anchor — the head sealed into the case manifest, which the builtin's own "does not cover" note names as the remedy for truncation — all returned true.
The fix#
The one sequence number a log of a given length may begin at is derivable, and
now it is derived. The last entry's sequence number is inside its own hash and so
is authenticated by whatever authenticates the head. The chain's memory cap is a
fixed rule — keep everything up to a bound, then drop a quarter of it whenever
the retained run refills — so from the last sequence number and the number of
retained entries there is exactly one legal first sequence number.
auditFirstRetainedFor reimplements that rule, auditEntrySeq accepts only a
positive integer, and the walk now fails at the first entry, naming the missing
range, when the first sequence number is not that value. It also requires the
second entry's link to be the genesis and requires consecutive sequence numbers.
The three copied numbers are gone: the count, the dropped count and the
completeness flag are computed from the entries.
What this does not mean, stated because the broader reading is tempting#
This did not allow a chosen event to be removed. The evidence isolates one tamper shape: removal of a contiguous run from the front of the retained log, with the remainder kept intact and in order. Editing or deleting a middle entry still broke the next entry's link against the recomputed hash — that comparison survived the defect untouched — and removing the tail changed the computed head, so the anchor comparison still failed. The defect hid the earliest events, in order, whatever they happened to be. It gave no ability to excise a chosen event, to remove recent events past an anchored check, or to add or alter any content.
This is not the forged-append defect. MVF-2026-0003 and
MVF-2026-0004 share one hunk in one commit and one
changelog entry describes both. They are two different defects with two
identifiers and two fixtures. Entries forged after the last genuine one are
MVF-2026-0004, not this.
Fixture#
builtin/audit_test.go, TestADeletedPrefixIsNotAnIntactLog,
TestACappedLogVerifiesAndACappedLogMissingMoreDoesNot and
TestTheCapRuleTheVerifierUsesIsTheOneTheChainFollows. The third is the one that
keeps the fix honest: it holds the cap rule the verifier derives from to the cap
rule the writer actually follows, so the two cannot drift into a verifier that
rejects legitimately capped logs.
Workarounds#
None is needed, and none should be inferred: no released version carries
audit_verify, so there is no affected deployment.
For a development build in the affected window the available check was partial. The pre-fix result reported both the walk's own link count and the document-copied entry count, so a log with its first entries deleted and its counts left alone showed those two numbers disagreeing. That does not cover the case that matters: rewriting the dropped count, the completeness flag and the first-retained-sequence field in the document made the two numbers agree and every boolean in the result say ok, and an automated gate keyed on any of them passed. A second partial check — that a nonzero dropped count can only be a multiple of the cap's quarter, and only once the chain has passed the bound, so a small dropped count is arithmetically impossible — catches the specific doctored document but requires the operator to know and hand-apply the cap rule, which is precisely the reasoning the fix had to internalise. The only complete measure in that window was operational and outside the tool: never let the party being audited have write access to the written log, which does nothing for a log already handed over.
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-0004together. - SECURITY.md for the severity scale.
- The advisory register.