fix(delete): guard the per-directory delete commit against dry-run

delete_plan_session_commit() lacked the central no-mutation guard that
manifest_delete_all() has, so a server-contacting -n run (or a hostile plan
frame) could still remove --delete-missing-args mirrors on the per-directory
timing path.  Return DELETE_COMMIT_OK immediately when the session is a
dry-run, and gate the receiver/server commit call sites too.  Add a unit test
that streams a plan naming an existing destination file and asserts it
survives.
This commit is contained in:
2026-09-17 01:02:22 +02:00
parent 946aa934cc
commit b02799327d
4 changed files with 65 additions and 1 deletions
+4
View File
@@ -489,6 +489,10 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
if (pending_plans) {
*pending_plans = plan_session;
plan_session = NULL;
} else if (config->dry_run) {
/* Central dry-run no-op: never commit a deletion for a -n run. */
delete_plan_session_destroy(plan_session);
plan_session = NULL;
} else {
DeleteCommitResult deletion = delete_plan_session_commit(plan_session, config);
bool limit = delete_plan_session_limit_reached(plan_session);
+5 -1
View File
@@ -970,7 +970,11 @@ void handler(int file_descriptor) {
arrived; with the disk writer drained, commit the deferred removals.
--delete-during already applied its plans on the receive thread. */
if (context->deferred_plans) {
DeleteCommitResult deletion = delete_plan_session_commit(context->deferred_plans, config);
/* Defence in depth (the enclosing block already excludes dry-run): a
-n run never commits a deletion. */
DeleteCommitResult deletion = config->dry_run ? DELETE_COMMIT_OK
: delete_plan_session_commit(
context->deferred_plans, config);
if (deletion == DELETE_COMMIT_ERROR) {
transfer_ok = false;
} else if (deletion == DELETE_COMMIT_LIMIT_REACHED) {
+5
View File
@@ -850,6 +850,11 @@ static bool apply_deferred_path(DeletePlanSession* session, const Config* config
DeleteCommitResult delete_plan_session_commit(DeletePlanSession* session, const Config* config) {
if (!session || !config)
return DELETE_COMMIT_ERROR;
/* Central no-mutation guard (mirrors manifest_delete_all): a dry-run never
deletes. The receive path already skips plan application, but a hostile or
buggy peer could still reach the commit, so treat it as a no-op. */
if (session->dry_run)
return DELETE_COMMIT_OK;
bool ok = true;
if (session->defer) {
for (int i = 0; i < session->deferred->size && ok; i++)