Skip to content

Make the nonce-store rename durable, not just its contents - #950

Merged
kwsantiago merged 2 commits into
mainfrom
nonce-store-durable-rename
Aug 7, 2026
Merged

Make the nonce-store rename durable, not just its contents#950
kwsantiago merged 2 commits into
mainfrom
nonce-store-durable-rename

Conversation

@kwsantiago

@kwsantiago kwsantiago commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Both nonce stores made their contents durable and not the name pointing at them. Syncing a temp file and renaming it leaves the rename itself unflushed, so a power loss can put the previous file back and silently undo the write.

For a nonce store that undo is the failure the store exists to prevent. A commitment claimed just before the loss reads as unclaimed on the next boot, the device signs a second time under the same nonce, and two shares over one nonce with different challenges recover the signer's key share. An earlier change closed the case where the contents never reached the disk; this closes the case where they did and the directory entry did not.

Three paths, not one. The rewrite path in each store, and the append path that creates the store on its first use. That last one is the one that matters: the rewrite in the network store only runs once a hundred thousand entries have accumulated, while the first record creates a name, and a name that never reaches the disk takes the whole store with it, so every session id reads as unconsumed after the next boot. Hardening the rare path and leaving the first-use path was the wrong half.

The helper already existed for key rotation and is now exported rather than copied a third and fourth time, since a duplicated durability primitive is how one copy ends up subtly different. Folded into that: it is a single documented function instead of a cfg pair, because with the crate denying missing docs and the item now publicly reachable, the undocumented Windows arm would have failed to compile there. Pull-request CI never builds Windows, so that would have landed on main and surfaced in a release build. It also now handles a single-component relative path, where parent() yields an empty string rather than nothing and opening it fails, matching a guard the signing path already applies to the same case.

The Windows behaviour is stated honestly as absent rather than provided elsewhere: a directory handle cannot be opened there through the standard library, and the rename primitive orders its metadata updates without promising they have reached the disk when it returns.

Test plan

The first version of this description said no test could discriminate, on the grounds that crash durability needs fault injection. That was wrong, and the correction is the useful part: the crash is untestable, but the property that matters is not. The caller signs on a successful claim, so a directory sync that fails has to fail the claim rather than be swallowed. Making the store directory writable and searchable but not readable produces exactly that, with nothing simulated, because opening the directory to sync it is what breaks.

Falsified: removing the sync makes that test fail with the message it was written for, and restoring it passes. The test skips itself if the mode does not actually take, since as root it would otherwise assert into a setup that never applied and pass regardless of the code.

All three crates' suites pass, the workspace builds, formatter and clippy clean.

Filed rather than fixed here

The larger hole in the same guard, found while reviewing this: the lock protecting the store is taken on the file that the rename then unlinks, so a second caller can hold a lock on a doomed inode, read the pre-claim state, and hand out the same commitment. It is pre-existing, unaffected by this change, and deserves its own change. Also filed: five further rename sites elsewhere with the same missing sync, and a credential writer that syncs but discards the error while documenting that it must not.

Summary by CodeRabbit

  • Reliability
    • Improved nonce-store durability by synchronizing directory changes after creating or replacing persisted data.
    • Failures during directory synchronization are now reported instead of being silently ignored.
    • Added platform-specific handling and validation for directory synchronization.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 457abb69-caec-494a-a4be-7148425b11c8

📥 Commits

Reviewing files that changed from the base of the PR and between 8377b18 and 18081e3.

📒 Files selected for processing (4)
  • keep-cli/src/signer/nonce_store.rs
  • keep-core/src/lib.rs
  • keep-core/src/rotation.rs
  • keep-frost-net/src/nonce_store.rs

Walkthrough

The change adds a public fsync_dir helper and applies directory synchronization after nonce store creation and atomic replacement. Existing-file appends still synchronize only the file. Unix tests verify directory-sync failures.

Changes

Nonce store directory durability

Layer / File(s) Summary
Directory synchronization helper
keep-core/src/rotation.rs, keep-core/src/lib.rs
fsync_dir is public. Non-Windows implementations synchronize the parent directory, while Windows returns success.
Nonce store persistence synchronization
keep-frost-net/src/nonce_store.rs, keep-cli/src/signer/nonce_store.rs
New stores and renamed replacement files synchronize their parent directories. The CLI test verifies that a directory-sync failure returns an error.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: wksantiago

Poem

I thump my paws on durable ground,
While nonce files settle safe and sound.
A directory sync, then off I hop,
No lost rename can make me stop.
🐇 The store now rests beneath the crop.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: making nonce-store renames durable through directory synchronization.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nonce-store-durable-rename

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kwsantiago

Copy link
Copy Markdown
Contributor Author

Four taken, and two of them correct things I asserted rather than checked.

The claim that no test could discriminate was wrong. I reasoned from "crash durability needs fault injection", which is true of the crash and not of the property that matters: the caller signs on a successful claim, so a sync that fails has to fail the claim. Making the directory writable and searchable but not readable produces that failure with nothing simulated. Removing the sync now fails that test with the message it was written for. Shipping a security fix on the strength of an argument for why it could not be tested is worse than shipping it untested, because the argument discourages the next person from looking.

The missing-docs risk is real, and I verified it rather than taking it on faith: removing the doc comment from the other arm makes the crate fail to compile, so the lint does fire through the re-export and the undocumented Windows arm would have broken the release build. Pull-request CI cannot catch it because Windows only builds on push. Restructured into one documented function rather than a cfg pair, which removes the failure mode instead of patching it.

The observation about which path was hardened is the sharpest of the set. The rewrite in the network store runs only after a hundred thousand entries; the append that creates the store runs on the first session. I fixed the rare one and left the common one, and the common one loses the entire store rather than one record. Now synced when the record creates the file, with the reasoning at the call site so the next reader does not have to rediscover which path fires when.

Also taken: the empty-parent case, matching the guard the signing path already applies, and the Windows wording, which said the guarantee was covered by the rename primitive when it is simply absent there.

Not taken here, deliberately: the lock-versus-rename inode race. It is pre-existing, this change does not touch it, and it is a larger hole in the same guard than anything fixed here. It is filed and should be the next change rather than a rider on this one.

@kwsantiago
kwsantiago merged commit 0664857 into main Aug 7, 2026
12 checks passed
@kwsantiago
kwsantiago deleted the nonce-store-durable-rename branch August 7, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant