fix(upgrade): verify binary replacement to prevent false upgrade failure (#230)#1000
Conversation
…ure (Gentleman-Programming#230) The gentle-ai self-binary upgrade reported a false red X failure in the TUI even when the binary was correctly downloaded and replaced, because atomicReplace mapped any bare rename error straight to UpgradeFailed with no positive check of what actually landed on disk. Some hardened self-replace environments (overlayfs, immutable distros, SELinux/AppArmor policies) can report a rename failure for the running binary even though the underlying replace completed. Two changes, both scoped to internal/update/upgrade/download.go: - atomicReplace performs a single atomic rename (os.Rename via the renameFn seam). On Unix this is atomic: on success dst is the new binary; on failure dst is left untouched (the previous binary, or absent on first install). The caller already guards against Windows, where a running binary cannot be renamed over. - replaceAndVerify trusts a reported rename success and, only when the rename reports an error, positively verifies dst's on-disk SHA256 against the hash of the extracted binary. It converts the reported error to success ONLY when the content provably matches, fixing issue Gentleman-Programming#230. Because the rename is atomic, a genuine failure leaves dst as the previous binary (never missing or half-written) and the real error is returned — a real failure is never masked as success. renameFn is a package-level var (mirroring the existing httpClient/lookPathFn testability pattern) so TestReplaceAndVerify can drive all paths cross-platform: a clean rename, a benign error after the bytes landed (the Gentleman-Programming#230 case), a genuine error with content mismatch (non-masking), and first install.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe upgrade download flow now hashes the extracted binary and verifies replacement via a ChangesVerified binary replacement in upgrade flow
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Download
participant HashFile
participant ReplaceAndVerify
participant RenameFn
participant Filesystem
Download->>HashFile: compute SHA256 of extracted binary
Download->>ReplaceAndVerify: call with src, dst, wantHash
ReplaceAndVerify->>RenameFn: rename src to dst
RenameFn->>Filesystem: attempt rename
Filesystem-->>RenameFn: success or error
alt rename succeeded
RenameFn-->>ReplaceAndVerify: nil error
ReplaceAndVerify-->>Download: success
else rename returned error
RenameFn-->>ReplaceAndVerify: error
ReplaceAndVerify->>HashFile: hash dst content
HashFile-->>ReplaceAndVerify: dst hash
alt dst hash matches wantHash
ReplaceAndVerify-->>Download: success
else mismatch
ReplaceAndVerify-->>Download: error
end
end
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
What
The gentle-ai self-binary upgrade showed a false ✗ failure in the TUI ("Upgrading gentle-ai via binary (X → Y)") even when the binary was correctly downloaded and replaced —
gentle-ai --versionconfirmed the new version and the binary timestamp matched.Why (#230)
The ✓/✗ marker is driven solely by the upgrade step's returned error. For the self-binary upgrade, that error came from
atomicReplace— a singleos.Rename— with no check of what actually landed on disk. On hardened self-replace environments (overlayfs, immutable distros, SELinux/AppArmor policies) the running binary's rename can report an error even though the replace completed, so a successful upgrade was reported as a failure.Fix (scoped to
internal/update/upgrade/download.go)atomicReplaceperforms a single atomic rename (via arenameFnseam for testability, mirroring the existinghttpClient/lookPathFnpattern). On Unix this is atomic: on successdstis the new binary; on failuredstis left untouched (the previous binary, or absent on first install). The caller already guards against Windows, where a running binary cannot be renamed over.replaceAndVerifytrusts a reported rename success, and only when the rename reports an error does it hash the on-diskdstand compare it to the SHA256 of the extracted binary — converting the reported error to success iff the content provably matches. A genuine failure returns the real error and leaves the previous binary intact.Non-masking guarantee: a reported error is converted to success only when the on-disk bytes equal the expected hash exactly. A real failure (missing/old/mismatched binary) is never reported as success.
Tests
TestReplaceAndVerify(runs cross-platform via therenameFnseam) covers:dstabsent) → success.go test ./internal/update/upgrade/...is green;gofmt/go vetclean. No changes to the TUI/executor/spinner code — the fix is confined to the download/replace path.Closes #230
Summary by CodeRabbit
Bug Fixes
Tests