Repository navigation
fix(firmware): keep blocked state consistent with firmware on disk - #327
Conversation
DELETE /v1/firmware/{v} answering 404 unblocked v even when a directory
for it was still on disk (it failed to load at boot, or an earlier
RemoveAll failed), so after a restart a rolled-back build could load
again unblocked. A 404 now unblocks only when neither <v> nor an
.old-<v>-* aside directory exists.
A ?replace=1 upload whose aside cleanup failed returned an error with the
new bytes already in place: the index kept the old SHA and the version
stayed blocked. The swap is now done once the new directory is renamed
in: index, SHA and unblock follow it, and the leftover aside is reported
alongside the 201 (logged by the handler; boot deletes it). If the new
directory cannot move in, the old one is moved back; if that rollback
fails too, the version leaves the index until boot restores the aside,
so /bin never serves bytes that disagree with the pinned SHA.
DELETE and retention eviction now remove a version's .old- copies too,
so a stale aside cannot bring a deleted version back unblocked at boot.
The store gains rename/removeAll seams; the new tests and the two
existing chmod-based ones inject failures through them.
Closes #325
Codex review(Run by Codex in a read-only sandbox; posted on its behalf.
|
Opus review
1. Medium: a partly failed DELETE (or eviction) can bring back the pre-replace build, unblocked, at boot
Reproduced:
Suggested fix: in 2. Low/medium: retrying a
|
Review follow-ups on #327: - purgeLocked removes a version's .old- asides first and keeps <v>/ if any fails, for DELETE and eviction alike. Before, a stuck aside plus a removed <v>/ let boot restore the pre-replace bytes without their block. - A ?replace=1 upload whose swap succeeds unblocks the version even when it was not indexed (the retry after a failed rollback). - A DELETE of an unindexed version purges its leftover dir and asides and unblocks only once that succeeds; a failed purge or an unreadable store dir answers 500 and keeps the block. The store gains a readDir seam. - Tests pin the eviction purge, the eviction error, the 404 read-error path, the restart-after-failed-delete/eviction cases and a 201 on a failed eviction cleanup over HTTP. - Docs: rollback-failure wording, the replace/unblock sentence, the 201 note in API.md and the .old- copies in the DELETE docs.
Review fixes (00271e5)
Mutation check, each one reverted on its own; every one fails at least one test:
|
Opus re-checkThis re-checks 00271e5 against the Opus and Codex reviews. I ran throwaway HTTP-level tests in a scratch copy, then deleted it. Nothing was pushed.
Original findings, reproduced
New or remaining
404 → 500 and clients
Nit
Verdict: the original findings are fixed and verified. #1 is the same bug class through a new path, so a small fix is worth making before merge. #2, #3 and the nit are optional. |
…ndexed Re-check follow-ups on #327: - swapInLocked's !exists path purges the version's .old- copies before <v>/ (instead of a bare RemoveAll of <v>/) and fails the upload if that purge fails, so a stuck aside plus a failed rename can no longer leave only the pre-replace bytes for boot to restore unblocked. - A DELETE or eviction whose purge fails keeps the index entry, so the version stays listed and the call can be retried; the entry goes only once the files are gone. - DELETE checks the target/in-use guard before purging, also for a version the index does not hold (409 instead of purging a target). - ARCHITECTURE: rewrapped, and the boot-restore sentence now states what the code guarantees.
Re-check fixes (1b20edb)
Mutation check, each one reverted on its own; every one fails at least one test:
|
Opus re-check 2This re-checks 1b20edb against the Opus re-check, the Opus review and the Codex review. I ran throwaway HTTP tests in a scratch copy, restarting with a fresh
Earlier findings, reproduced over HTTP plus a restart
New
Clients
Checked, OK
Verdict: 1 blocker. Every earlier finding is fixed and reproduced. Fix finding 1 before merge: the change that keeps the entry introduced it, and the fix is small. The doc items in finding 2 are optional. |
Re-check 2 on #327: keeping the index entry after a failed purge let a RemoveAll that stopped part-way leave an indexed version without cinder.bin, which /bin could not serve while available, auto offers and a same-bytes re-upload (200, "verified") still trusted it. purgeLocked now renames each copy to a .tmp-<version>-* name, .old- asides first and <version>/ last, and stops at the first failed rename. A failed rename touches nothing, so the entry stays indexed, whole and blocked and the call can be retried. Once <version>/ is renamed away the version is gone: the entry is dropped and the block cleared, and the retired dirs are deleted best effort, logging a leftover. Boot already sweeps .tmp- dirs (a non-semver name never loads) and asideVersion never matches them, so boot cannot restore a retired copy. The store takes the app logger for that warning. Tests inject rename failures for the stuck cases, cover a RemoveAll that deletes cinder.bin and then fails (DELETE, eviction, HTTP), a same-bytes re-upload after it, and a whole retired copy that boot must not load. Docs: DELETE wording, "stays blocked and listed", the retire design and the rewrapped Record paragraph.
Re-check 2 fixes (656eca5)
Mutation check: going back to |
Opus re-check 3This re-checks 656eca5 against re-check 2 and every earlier round. I ran throwaway HTTP tests in a scratch copy, restarting with a fresh
Earlier findings, reproduced over HTTP plus a restart
Mutation: retiring New design, probed
Low / nits (none block)
Verdict: approve. Every finding from all earlier rounds is fixed and reproduced over HTTP plus a restart. In the new design, the holes I found need a filesystem failure, a crash or both, and none can resurrect a blocked build or serve a broken |
Closes #325
What and why
removeunblocks on 404 only when neither<v>/nor an.old-<v>-*aside dir exists (a dir that failed to load at boot, or an earlier failedRemoveAll, keeps the block, so a restart can't bring back a rolled-back build unblocked)..tmp-dir and swapped it in via an.old-aside. Fixed the failure handling:/binfor a missing dir), block kept; boot restores the aside;.old-copies too. Before, a leftover aside plus a later DELETE meant boot would restore the deleted version, unblocked.Store gains
rename/removeAllseams (defaultsos.Rename/os.RemoveAll); new tests and the two existing chmod-based tests inject failures through them (no permission tricks, no root skip).Docs: API.md, openapi.yaml (DELETE/upload descriptions), ARCHITECTURE "Knob firmware updates".
Tests
TestFirmwareStoreDeleteOfAnUnindexedVersionUnblocksOnlyWithoutFilesOnDiskTestFirmwareStoreReplaceCommitsWhenTheOldCopyCannotBeRemovedTestFirmwareStoreDeleteRemovesALeftoverOldCopyTestFirmwareStoreReplaceRollsBackWhenTheNewCopyCannotMoveInTestFirmwareStoreReplaceWhoseRollbackFailsHidesTheVersionUntilBootRestoresItTestFirmwareDeleteOfAnUnloadedVersionKeepsItBlocked,TestFirmwareReplaceWithAStuckOldCopyStillAnswersCreatedAndUnblocksMutation check: reverting the 404 check or the "swap done" return makes 4 of the store tests fail.
Evidence
Not fixed here (follow-ups)
putELFcreates its temp file inside<v>/without the store lock; a concurrent DELETE'sRemoveAllcan race it (ENOTEMPTY), leaving the version out of the index, dir on disk, block kept. Consistent after this PR (404 won't unblock; boot sweeps the dir) but the DELETE answers 500.<v>/exists but is invalid (no meta) and a valid.old-<v>-*exists, the aside is deleted before<v>/is swept, losing both. Only reachable by external corruption.?replace=1of an indexed version unblocks. Probably intended.