test(safety-rules): repro for #706 — on_disk_storage no-fsync enables double-vote - #743
test(safety-rules): repro for #706 — on_disk_storage no-fsync enables double-vote#743keanji-x wants to merge 1 commit into
Conversation
… double-vote Demonstrates gravity-audit Galxe#706 (F4): safety-rules last_voted_round is persisted via the on_disk_storage backend (production default, cluster/templates/validator.yaml.tpl) whose OnDiskStorage::write does File::create(temp) -> write_all -> fs::rename with NO sync_all/fsync of the file or its directory (gravity-aptos@e9544c8 secure/storage/src/on_disk.rs). A power-loss crash can therefore revert secure_storage.json to a lower last_voted_round while the durable consensusdb last_vote (schemadb set_sync(true)) and the wire-sent vote survive, enabling a second, conflicting vote in the same round = AptosBFT safety violation. New tests (in the compiled aptos-safety-rules crate, the production code): on_disk_write_is_not_durable_unlike_consensusdb Exercises the buggy OnDiskStorage::write and shows a "succeeded" write is byte-revertible (no fsync), contrasted with consensusdb set_sync(true). power_loss_revert_of_safety_data_enables_double_vote End-to-end through the real SafetyRules vote path: vote in round R, revert the persisted SafetyData to its pre-vote value (models the lost, never-fsync'd page), reload, and successfully sign a SECOND conflicting vote in round R. Includes a control proving the guard works when the state is intact (the conflicting proposal is refused). cache_off_safety_data_read_is_broken Bonus finding surfaced while building the repro: with enable_cached_safety_data=false, PersistentSafetyStorage::safety_data() always fails ("unknown variant `epoch`, expected `Ok` or `Err`") due to a misplaced `?` that infers the get value type as Result<SafetyData,Error>. Relevant to Galxe#706's recovery fix, which would run on this cache-off path. Honest scope: a unit test cannot lose a real page on power-loss; the revert *models* that loss. What is proven is the no-fsync write path, the byte-revertibility, and that a SafetyData revert deterministically yields a double-vote. Proposed fix (not applied here, to keep this test-only): fsync the temp file and its containing directory before/after the rename in OnDiskStorage::write; on recovery re-tighten last_voted_round to >= the durable consensusdb last_vote round. No production code modified. Refs Galxe/gravity-audit#706 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I think there are two separate points here:
A proper fix should sync the temp file before rename and fsync the parent directory after rename, so both the file contents and the rename are durable.
However, the real validator path has another layer: after SafetyRules signs, Because of that, I think the fsync fix is still reasonable and low-risk, but the current unit test is more a modeled SafetyRules-only consequence than a full production repro. To prove the double-vote consequence, the test would need to drive the restart/RoundManager path with consensusdb |
|
This PR is stale because it has been open 45 days with no activity. Remove the |
What
Reproduction / demonstration for gravity-audit #706 (F4 in
galxe/RESTART_RECOVERY_FINDINGS.md): the safety-ruleslast_voted_round— the only guard against a validator voting twice in a round — is persisted via theon_disk_storagebackend (the production default,cluster/templates/validator.yaml.tpl:8-11,type: on_disk_storage), whose write path performs no fsync, so a power-loss crash can revert it and enable a double-vote (BFT safety violation).The buggy write (
gravity-aptos@e9544c8secure/storage/src/on_disk.rs, the rev compiled perCargo.lock):There is no
sync_all/fsyncanywhere in the crate. By contrast the consensus DB is durable: schemadb'sdefault_write_options()callsopts.set_sync(true)(storage/schemadb/src/lib.rs:331-333). So on power-loss the durable consensusdblast_vote+ the wire-sent vote survive whilesecure_storage.jsonreverts to a lowerlast_voted_round.enable_cached_safety_datadefaults totrue, so the gap is invisible in-process.Tests added
All three live in the compiled
aptos-safety-rulescrate (the production code, a workspace path dep —Cargo.toml:88), not a throwaway harness.on_disk_write_is_not_durable_unlike_consensusdb— exercises the buggyOnDiskStorage::writeand shows that a write which returnedOkis byte-revertible (no fsync), contrasted with consensusdb'sset_sync(true).power_loss_revert_of_safety_data_enables_double_vote— end-to-end through the realSafetyRulesvote path: vote in round R (persistinglast_voted_round=R,last_vote=Some(..)), then revert the persistedSafetyDatato its pre-vote value (models the lost, never-fsync'd page), reload, and successfully sign a second, conflicting vote in round R. Includes a control proving the guard works when the state is intact (the conflicting proposal is refused, the prior vote is returned).cache_off_safety_data_read_is_broken— bonus finding surfaced while building the repro: withenable_cached_safety_data=false,PersistentSafetyStorage::safety_data()always fails withunknown variant \epoch`, expected `Ok` or `Err`because a misplaced?makes the genericgetinfer its value type asResult<SafetyData, Error>. A directinternal_store().get::(SAFETY_DATA)` reads the same bytes fine. This is relevant to fix(cluster): default genesisTimestampSecs so e2e survives genesis-tool strict mode #706's recovery fix, which would re-read SafetyData on the (currently broken) cache-off path.Evidence
Run with:
Honest scope (proven vs. requires real power-loss)
SafetyDatarevert, the real safety-rules vote path deterministically signs a second conflicting vote in the same round.Proposed fix (NOT applied here — kept test-only)
OnDiskStorage::write,sync_all()the temp file andfsyncthe containing directory before/afterfs::rename(or backSAFETY_DATAwith a sync-durable store).last_voted_roundto>=the durable consensusdblast_voteround, so a reverted secure-storage cannot regress below what was already committed/sent.safety_data()read (Ok(self.internal_store.get(SAFETY_DATA)?.value)), which the recovery cross-check would depend on.No production code was modified.
Refs Galxe/gravity-audit#706
🤖 Generated with Claude Code