fix: preserve private Windows ACLs through backup, restore, and quarantine journal - #960
Conversation
Plain `fs::copy` let every plain-copied backup artifact, above all `profile-identity.json`, inherit the destination directory's ACL on Windows (three inherited ACEs, default token owner, unprotected DACL). The strict private reader then refused the copied record in both the backup root (`load_verified_backup`) and the restored profile, so every rehearsal failed at ACL admission before reaching its named fault boundary. The marker `NotFound` and "injected fault" assertion failures were derivatives of that first refusal. Copy plain artifacts through `tracedecay_private_fs::create_private_file` on both the backup and restore sides, create the manifest and rehearsal marker the same way, and give the restore staging directory its Windows private analogue of the existing 0700 mode. Bytes are preserved exactly and the source is never re-permissioned. Fixtures: seed the verified-backup profile identity through the daemon's record authority so the test reaches the backup contract on Windows, and make the fault-rehearsal tests report the concrete earlier refusal when the injected boundary or its publication marker never appears. Assert the copied identity is admitted by the strict reader, not merely byte-equal. Refs #920 Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
The quarantine journal was written with `PrivateStoreIo` (plain create on Windows, inheriting the parent's ACL) but read back through `DatabaseAuthority::read_record_strict`, which admits only the exact protected current-user DACL. Every restart-recovery test therefore failed at "secure remote restore quarantine fence before read". Publish the fence through `DatabaseAuthority::publish_record_atomically`, the same private record authority the reader pairs with, clearing this attempt's own stale staging record first so an interrupted write never blocks the next one. Refs #920 Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
The negative schema tests wrote the record with `fs::write`, so on Windows the reader refused it at private-file admission and the "unknown field" assertion never exercised parsing. Publish the fixture through the same record authority the daemon mints with. Refs #920 Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
|
|
This branch no longer merges cleanly with |
# Conflicts: # crates/tracedecay-maintenance/src/profile_backup.rs # crates/tracedecay-maintenance/src/profile_backup/snapshot.rs # crates/tracedecay/tests/product_surface_suite/verified_profile_backup.rs
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
Architecture review — HOLD for ownership proof, conflicts and Windows evidence
Reviewed 07a782cfcb4768758929a3efc0583e3960552498; source review only. GitHub reports conflicts; main CI is queued and hotpath coverage failed, so this is not verified merge-state evidence.
Creating backup copies, manifests and restore fences through the private-file authority is correct. The source must remain untouched, destination create-new semantics must remain strict, and the real private reader—not byte equality alone—is the acceptance boundary.
P1: remote_recovery/publication/quarantine.rs::write unconditionally removes the shared fence.with_extension("staging") before publishing. Its comment calls that file 'this attempt's own', but the path does not establish ownership. Make the existing exclusive restore/terminal-vacancy fence explicit at this operation and prove no live publisher can own that staging file, or use an attempt-qualified staging record whose cleanup verifies ownership. Add concurrent/interrupted publisher and foreign-staging tests. Do not unlink another live attempt's file to make create-new succeed.
P1 acceptance: native Windows backup -> restore -> strict identity read; wrong-owner/unprotected-DACL rejection without repair; partial copy/sync failure with no published manifest; resumed restore does not overwrite a foreign destination; and quarantine recovery preserves the exact terminal vacancy. An x86_64-pc-windows-gnu compile check is not DACL verification.
Resolve the current #707 conflicts alongside #946's handle-lifetime fixes without globally closing owners or broadening permissions. Classify the hotpath failure on the final tree rather than disabling coverage. No #707 draft or release change.
|
Resolved against the current redesign base and pushed as Conflict decisions:
Verification on the integrated tree: 8 maintenance backup tests, 12 remote-recovery tests, 4 profile-identity tests, and 10 product backup journeys passed; scoped clippy reported 0 warnings; actionlint and rustfmt passed; Windows GNU cross-check completed with 0 errors (13 pre-existing cfg-target warnings outside this diff). Issue #920 remains open pending real Windows CI acceptance. |
|
Final base resync: pushed |
|
Final ancestry confirmation: pushed |
…sor/fix-920-windows-acl-backup-dfec
…sor/fix-920-windows-acl-backup-dfec
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
20e693d
into
codex/tracedecay-total-redesign-plan-reopened
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
System-level re-review at e310f7e27 — narrowed after tracing the restore owner
The current remaining diff is substantially smaller than the original proposal: private creation for manifest/rehearsal records, Windows staging-directory privacy, private quarantine publication, and the associated tests. Do not reintroduce duplicate copy helpers already absorbed into the target.
I traced RemoteRecoveryPublicationContextV1::publish_restore: it validates the registered binding, reserves the project replacement, quiesces the project, retires the exact Graph/Store pair, obtains the terminal vacancy, then writes the quarantine fence before awaiting destructive maintenance. This matters: the journal records an irreversible owner-retirement boundary; it is not just a backup status file. Keep that ordering, and keep post-swap recovery bound to the exact published/rollback file identities. A later cancellation cannot restore the already-closed owner by inference.
Correction to the earlier staging comment: the fixed staging filename alone is not proof of two legitimate concurrent publishers. There is an existing per-project replacement/recovery ownership protocol. I am not requesting another global mutex or presenting a reproduced concurrent overwrite here.
Remaining P2 ownership/acceptance gap: quarantine::write still deletes whatever occupies the fixed .staging path before private creation, and its new test only seeds arbitrary bytes there and expects deletion. Make the existing exclusion and ownership premise explicit at this boundary. Exercise the actual replacement/recovery path with an interrupted publisher and competing admission, proving only the exact restore owner can clear its scratch record and that a foreign/newer authoritative fence is untouched. If that exclusion cannot cover every writer of this path, use attempt-owned scratch publication rather than deletion by filename. Do not broaden cleanup or weaken private-file validation.
The ACL direction is appropriate: create new artifacts with the policy the strict reader requires, rather than repairing arbitrary input on read. For staging-directory permission changes, retain the premise that this is the newly created restore directory, not an already-existing foreign directory. Native Windows verification must reopen the produced identity/fence through the real strict reader and exercise interruption on both sides of publication; byte equality alone is insufficient.
No additional confirmed production defect was established in the inspected private-creation changes. Exact-head CI is pending and hotpath runs queued; this review is source analysis, not Windows execution or a release approval.
Performance Comparison
|
Summary
profile-identity.json), the backup manifest, and the rehearsal marker throughtracedecay_private_fs::create_private_file, so the copies carry the exact owner-private mode/DACL the strict readers admit instead of what the destination directory would letfs::copyinherit.DatabaseAuthority::publish_record_atomically, the private record authority paired with theread_record_strictreader that was refusing it.Fixes #920. Targets
codex/tracedecay-total-redesign-plan-reopened(#707); #707 stays draft.Motivation
In the #707 Windows baseline (run 34039753429) 16 rows failed with
private Windows DACL validation failed: SecuritySnapshot { owner_is_current_user: false, dacl_is_protected: false, ace_count: 3, ... }on either the copiedprofile-identity.jsonor the freshly writtensessions.remote-restore-quarantine.json. Three distinct defects, one root class: a file was created by a plain OS primitive (fs::copy,OpenOptions::create) and then read by the strict private authority.profile_backup::tests::*(3),profile_backup_rehearsal_test::*(7),verified_profile_backup::*(2)fs::copyofprofile-identity.jsoninto the backup root / restore staging (snapshot_artifact,copy_verified_file);verified_profile_backupalso seeded the source identity withfs::writeNotFound, "injected rehearsal publication fault" assertion missesquarantine::tests::*(3)PrivateStoreIo::write_file_atomically_durablecreates the fence with the inherited ACL;read_record_strictrefuses itprofile_identity::tests::unknown_fields_and_schema_versions_fail_closedfs::writerefused at admission before parsingChanges
crates/tracedecay-maintenance/src/profile_backup.rscopy_private_artifact: opens the source read-only, creates the destination viatracedecay_private_fs::create_private_file, streams bytes,sync_alls, and removes the partial destination on failure. Used bycopy_verified_file(restore) andsnapshot::snapshot_artifact's plain-copy branch (backup). Bytes and durable identity are unchanged; the source is never re-permissioned.write_new_synced(manifest + rehearsal marker) creates through the same authority (stillcreate_newsemantics, 0600 on Unix).restrict_private_directorygains its Windows analogue viamake_private_directoryon the just-created restore staging directory. This is the same "make our own new directory private" step as the existingchmod 0700, not a repair of foreign material.crates/tracedecay-store-runtime/.../publication/quarantine.rs:writepublishes throughDatabaseAuthority::publish_record_atomicallywith the record name shared with the reader; clears this attempt's own leftover.stagingfirst so an interrupted write cannot block the next (the previous truncating write tolerated that too). New testinstall_replaces_a_stale_staging_record_left_by_an_interrupted_write.crates/tracedecay-runtime-core/src/storage/profile_identity.rs(tests):write_identitypublishes viapublish_record_atomically.crates/tracedecay/tests/product_surface_suite/verified_profile_backup.rs:seed_profilepublishes the identity viapublish_record_atomically.crates/tracedecay/tests/product_surface_suite/profile_backup_rehearsal_test.rs:rehearse_until_injected_fault/interrupt_published_rehearsalhelpers assert the named boundary was reached and include the actual error when the marker is missing.crates/tracedecay-maintenance/src/profile_backup/tests.rs: asserts the backup-root and restored identity records are admitted byread_existing_profile_identity_record, not merely byte-equal.No private-fs check was weakened; wrong-owner / unprotected-DACL files are still refused (
permissive_existing_file_is_rejected_without_acl_rewrite,rehearsal_rejects_identity_tampered_backup_material, andinsecure_or_symlinked_identity_fails_closedare unchanged).Test plan
cargo test -p tracedecay-maintenance --lib profile_backup(8/8),cargo test -p tracedecay-store-runtime --lib remote_recovery(12/12 incl. new test),cargo test -p tracedecay-runtime-core --lib profile_identity(4/4),cargo test -p tracedecay --test product_surface_suite -- profile_backup(10/10)cargo clippy --all-targets --locked -- -D warningsontracedecay-maintenance,tracedecay-store-runtime,tracedecay-runtime-core, and theproduct_surface_suitetest target: cleancargo fmt --all -- --check: cleancargo check --target x86_64-pc-windows-gnu --testsfor the three library crates: clean (thecfg(windows)branch compiles against themake_private_directoryre-export). Thetracedecaycrate itself cannot be cross-checked here (orthas no windows-gnu prebuilt); its edits are platform-neutral.Checklist
CHANGELOG.mdupdated (not touched: feat: TraceDecay V2 delivery (PR8–PR13) + daemon performance, memory, and reliability overhaul #707 integration branch; leave to the branch owner).envfiles included