You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
M4B (PR #39) merged with 7 actionable bot findings deferred to follow-up. The PR also merged with red CI (workspace-wide cargo fmt drift in pre-existing files outside M4B's scope — see #cargo-fmt below).
Issue: When the .cfsave.checksum sidecar is absent (e.g., attacker deletes it), the loader treats expected_checksum as None and skips verification. Defeats the entire "Save corruption is detectable" promise.
Fix sketch: Require the sidecar; return new SaveError::MissingChecksum { path } variant when absent or empty. Add SaveError::Io { path, source } for clean filesystem errors so they don't masquerade as MigrationFailed.
Issue: When capacity is hit and an event is dropped or a cosmetic is evicted, the encoder has already advanced the chain state. The on-disk events.jsonl no longer matches the chain — verification reports false tampered on legitimate bundles under recorder pressure.
Two viable fixes:
(a) Defensive guard (small): Recorder::enable_chain_mode panics when capacity is non-zero. Chain mode = unlimited capacity by contract. ~10 LOC.
(b) Real refactor (correct but larger): Defer chain computation to write time. Remove eager encoding from record_with_cosmetic; have cf-replay::write_run_bundle walk the final retained events and stamp prev_event_hash + chained_hash_hex over them. ~80-100 LOC + test updates.
Recommendation: ship (a) first as a follow-up PR (zero risk); land (b) when M11/M12 refactor settles.
P2 (Codex medium bugs)
3. cfctl save migrate <path> ignores the file path argument
Issue: Always migrates quicksave.cfsave in path.parent(), regardless of what <path>.cfsave the user supplied. In a multi-save directory it migrates the WRONG file while printing a misleading success envelope.
Fix sketch: Read the exact file at path, deserialize as WorldSave, call migration::migrate(blob, target) directly, write back to the same path with a fresh sidecar checksum. Refuse non-current --to <version> until partial-migration handlers ship.
4. Ledger chain doesn't bind event_id into hash material
Issue: Accepts event_id but hash is only (prev_hash, payload). Rewriting an event_id field in events.jsonl passes verify_chain unchanged, even though event_id is part of replay/audit semantics (M10 cause-chain walker, M4 envelope schema).
Issue: Same root cause as Refresh Rust toolchain, CI actions, and direct crates #3 — cf_save::quicksave::read_quicksave always migrates to CURRENT_SAVE_SCHEMA_VERSION internally, so the post-check outcome.save.schema_version != requested always fails for any non-current target. The flag accepts every input then rejects every value.
Fix sketch: Same shape as Refresh Rust toolchain, CI actions, and direct crates #3 — read the file directly, call migration::migrate(blob, target) with the parsed --to value, write back. Reject targets newer than the current build's schema.
6. cf-mod save validate reports blake3_hex: Some("") when sidecar missing
Issue: When the .checksum sidecar is absent, expected_checksum.unwrap_or_default() yields an empty string. The audit envelope reports blake3_hex: Some("") — a nominally-present-but-meaningless hash. The actual canonical-JSON BLAKE3 of the (possibly migrated) save is never computed.
Fix sketch: When sidecar missing, compute outcome.blob.checksum_hex()? and report THAT in the envelope. Distinguish in the report between "verified against sidecar" and "computed (no sidecar)".
Devin (1 bug + 7 informational; only the bug requires action)
Issue: When recorded save_schema_version differs from current, banner unconditionally prints (handler: v1_to_v2). If a v2.0.0 bundle is opened by a v3.0.0 build (with a v2_to_v3 handler), it would incorrectly display Replay migrated from v2.0.0 -> v3.0.0 (handler: v1_to_v2).
Fix sketch: Either omit the parenthetical entirely OR call cf_save::migration::migrate(blob, current) on a stub deserialized from the bundle and surface the actual outcome.handler_chain in the banner.
Informational analyses (NO action required)
Devin also flagged 7 informational findings; all are benign and intentional per the M4B design:
fire_quickload pattern-match is safe because SaveSchemaVersion derives Copy
is_multiple_of(cadence) is guarded by the early cadence == 0 return
WorldSave::deserialize checksum verifies re-serialized canonical form (intentional, deterministic via BTreeMap key ordering + DR-052 float rules)
All 3 platforms (ubuntu / macos / windows) failed at cargo fmt --check. Diff is in pre-existing files outside M4B scope (cf-actor/src/gib.rs, cf-actor/src/sim.rs) AND new M4B code (cf-app/src/main.rs, etc.). Single-line fix: cd game && cargo fmt --all then commit + push. Should land as the same follow-up PR that addresses the 7 findings above.
Recommended ordering for the follow-up PR(s)
Tiny unblock PR — cargo fmt --all only. Gets CI green for main.
M4B (PR #39) merged with 7 actionable bot findings deferred to follow-up. The PR also merged with red CI (workspace-wide
cargo fmtdrift in pre-existing files outside M4B's scope — see #cargo-fmt below).Bot findings to address
P1 (Codex blocker bugs)
1.
read_quicksavesilently allows missing checksum sidecargame/crates/cf-save/src/quicksave.rs::read_quicksave.cfsave.checksumsidecar is absent (e.g., attacker deletes it), the loader treatsexpected_checksumasNoneand skips verification. Defeats the entire "Save corruption is detectable" promise.SaveError::MissingChecksum { path }variant when absent or empty. AddSaveError::Io { path, source }for clean filesystem errors so they don't masquerade asMigrationFailed.read_refuses_when_checksum_sidecar_is_missingread_refuses_when_checksum_sidecar_is_emptymissing_save_file_returns_io_error_not_migration_failed2. Chain encoder advances BEFORE backpressure drop handling
game/crates/cf-replay/src/lib.rs::record_with_cosmeticcapacityis hit and an event is dropped or a cosmetic is evicted, the encoder has already advanced the chain state. The on-diskevents.jsonlno longer matches the chain — verification reports falsetamperedon legitimate bundles under recorder pressure.Recorder::enable_chain_modepanics when capacity is non-zero. Chain mode = unlimited capacity by contract. ~10 LOC.record_with_cosmetic; havecf-replay::write_run_bundlewalk the final retained events and stampprev_event_hash+chained_hash_hexover them. ~80-100 LOC + test updates.P2 (Codex medium bugs)
3.
cfctl save migrate <path>ignores the file path argumentgame/crates/cfctl/src/main.rs::cmd_save_migratequicksave.cfsaveinpath.parent(), regardless of what<path>.cfsavethe user supplied. In a multi-save directory it migrates the WRONG file while printing a misleading success envelope.path, deserialize asWorldSave, callmigration::migrate(blob, target)directly, write back to the same path with a fresh sidecar checksum. Refuse non-current--to <version>until partial-migration handlers ship.4. Ledger chain doesn't bind
event_idinto hash materialgame/crates/cf-save/src/ledger_chain.rs::Encoder::compute_hashevent_idbut hash is only(prev_hash, payload). Rewriting anevent_idfield inevents.jsonlpassesverify_chainunchanged, even thoughevent_idis part of replay/audit semantics (M10 cause-chain walker, M4 envelope schema).event_idinto the hash material:prev_hash || '|' || event_id || '|' || canonical_payload. Add regression test:verify_rewriting_event_id_returns_tampered.Cursor (medium/low bugs)
5.
cf-headless save migrate --to <version>flag is brokengame/crates/cf-headless/src/save_migrate.rs::run_migratecf_save::quicksave::read_quicksavealways migrates toCURRENT_SAVE_SCHEMA_VERSIONinternally, so the post-checkoutcome.save.schema_version != requestedalways fails for any non-current target. The flag accepts every input then rejects every value.migration::migrate(blob, target)with the parsed--tovalue, write back. Reject targets newer than the current build's schema.6.
cf-mod save validatereportsblake3_hex: Some("")when sidecar missinggame/crates/cf-mod/src/save_validate.rs(lines 101-117).checksumsidecar is absent,expected_checksum.unwrap_or_default()yields an empty string. The audit envelope reportsblake3_hex: Some("")— a nominally-present-but-meaningless hash. The actual canonical-JSON BLAKE3 of the (possibly migrated) save is never computed.outcome.blob.checksum_hex()?and report THAT in the envelope. Distinguish in the report between "verified against sidecar" and "computed (no sidecar)".Devin (1 bug + 7 informational; only the bug requires action)
7. Viewer migration banner hardcodes
(handler: v1_to_v2)game/crates/cf-tools-replay-viewer/src/viewer.rs(lines 121-125)save_schema_versiondiffers from current, banner unconditionally prints(handler: v1_to_v2). If a v2.0.0 bundle is opened by a v3.0.0 build (with av2_to_v3handler), it would incorrectly displayReplay migrated from v2.0.0 -> v3.0.0 (handler: v1_to_v2).cf_save::migration::migrate(blob, current)on a stub deserialized from the bundle and surface the actualoutcome.handler_chainin the banner.Informational analyses (NO action required)
Devin also flagged 7 informational findings; all are benign and intentional per the M4B design:
fire_quickloadpattern-match is safe becauseSaveSchemaVersionderivesCopyis_multiple_of(cadence)is guarded by the earlycadence == 0returnWorldSave::deserializechecksum verifies re-serialized canonical form (intentional, deterministic viaBTreeMapkey ordering + DR-052 float rules)delta::diffdoesn't recurse into nested objects within arrays (intentional flat replacement; bench still hit 12.25x)emit_m4b_snapshot_for_tickmulti-lock pattern is TOCTOU-safe becausedrive_tickis single-threadedsave_validate.rschecksum sidecar path construction is correct for both.cfsaveand no-extension inputsCI status (red on PR #39)
All 3 platforms (ubuntu / macos / windows) failed at
cargo fmt --check. Diff is in pre-existing files outside M4B scope (cf-actor/src/gib.rs,cf-actor/src/sim.rs) AND new M4B code (cf-app/src/main.rs, etc.). Single-line fix:cd game && cargo fmt --allthen commit + push. Should land as the same follow-up PR that addresses the 7 findings above.Recommended ordering for the follow-up PR(s)
cargo fmt --allonly. Gets CI green formain.References
specs/done/M4B.md