feat: exclude basis directories from --delete

Generalize the delete walker's protected-root-child skip into a prefix list.
The receiver now passes both the --delay-updates staging directory and every
basis-dir path, so a --delete run can never treat a basis snapshot (which a
--link-dest run just linked from) as destination content to remove.
This commit is contained in:
2026-09-06 18:47:29 +02:00
parent d38920c972
commit 8f846a43b8
2 changed files with 34 additions and 17 deletions
+27 -12
View File
@@ -204,9 +204,22 @@ static bool is_dir_in_manifest(const char* rel_path, ArrayList* manifest) {
return false; return false;
} }
/* True when the relative path is, or lies below, one of the protected
prefixes. A prefix "a" therefore protects "a" and "a/b/c" but not "ab". */
static bool path_under_skip_prefix(const char* rel_path, const char* const* prefixes,
int prefix_count) {
for (int i = 0; i < prefix_count; i++) {
size_t prefix_len = strlen(prefixes[i]);
if (strncmp(rel_path, prefixes[i], prefix_len) == 0 &&
(rel_path[prefix_len] == '\0' || rel_path[prefix_len] == '/'))
return true;
}
return false;
}
static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifest, static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifest,
size_t max_delete, size_t* deleted_count, size_t max_delete, size_t* deleted_count,
const char* skip_root_child) { const char* const* skip_prefixes, int skip_prefix_count) {
int scanfd = dup(dirfd); int scanfd = dup(dirfd);
if (scanfd < 0) if (scanfd < 0)
return false; return false;
@@ -220,18 +233,19 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes
while ((entry = readdir(dir)) != NULL) { while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue; continue;
/* A --delay-updates run keeps its staging directory as a direct child of
the receive root. Its contents are not manifest entries yet (they are
published after deletion), so descending into it would delete every
staged file as an "extra". Skip only the top-level staging name; nested
directories with the same name are ordinary destination content. */
if (rel_path[0] == '\0' && skip_root_child && strcmp(entry->d_name, skip_root_child) == 0)
continue;
char* child_rel = path_cat((char*)rel_path, entry->d_name); char* child_rel = path_cat((char*)rel_path, entry->d_name);
if (!child_rel) { if (!child_rel) {
operation_ok = false; operation_ok = false;
continue; continue;
} }
/* A --delay-updates run keeps its staging directory below the receive
root, and basis-dir snapshots live there too. Their contents are not
manifest entries, so descending into them would delete every staged /
basis file as an "extra". */
if (path_under_skip_prefix(child_rel, skip_prefixes, skip_prefix_count)) {
free(child_rel);
continue;
}
struct stat st; struct stat st;
if (fstatat(dirfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0) { if (fstatat(dirfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0) {
if (errno != ENOENT) if (errno != ENOENT)
@@ -249,7 +263,7 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes
bool child_removed = false; bool child_removed = false;
if (childfd >= 0) { if (childfd >= 0) {
child_removed = delete_extras_fd(childfd, child_rel, manifest, max_delete, deleted_count, child_removed = delete_extras_fd(childfd, child_rel, manifest, max_delete, deleted_count,
skip_root_child); skip_prefixes, skip_prefix_count);
if (!child_removed) if (!child_removed)
operation_ok = false; operation_ok = false;
close(childfd); close(childfd);
@@ -301,7 +315,7 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes
} }
bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete, bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete,
const char* skip_root_child) { const char* const* skip_prefixes, int skip_prefix_count) {
if (!manifest) if (!manifest)
return false; return false;
int rootfd; int rootfd;
@@ -318,14 +332,15 @@ bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t ma
if (rootfd < 0) if (rootfd < 0)
return false; return false;
size_t deleted_count = 0; size_t deleted_count = 0;
bool ok = delete_extras_fd(rootfd, "", manifest, max_delete, &deleted_count, skip_root_child); bool ok = delete_extras_fd(rootfd, "", manifest, max_delete, &deleted_count, skip_prefixes,
skip_prefix_count);
if (close(rootfd) != 0) if (close(rootfd) != 0)
ok = false; ok = false;
return ok; return ok;
} }
bool delete_extras(const char* dest_root, ArrayList* manifest) { bool delete_extras(const char* dest_root, ArrayList* manifest) {
return delete_extras_limited(dest_root, manifest, SIZE_MAX, NULL); return delete_extras_limited(dest_root, manifest, SIZE_MAX, NULL, 0);
} }
bool has_path_traversal(const char* path) { bool has_path_traversal(const char* path) {
+7 -5
View File
@@ -10,12 +10,14 @@ char* output_escape(const char* string, bool eight_bit_output);
char* path_cat(const char* path1, const char* path2); char* path_cat(const char* path1, const char* path2);
bool glob_match(const char* pattern, const char* str); bool glob_match(const char* pattern, const char* str);
bool delete_extras(const char* dest_root, ArrayList* manifest); bool delete_extras(const char* dest_root, ArrayList* manifest);
/* Remove files/dirs under dest_root that are not listed in manifest. When /* Remove files/dirs under dest_root that are not listed in manifest. The
skip_root_child is non-NULL, a direct child of dest_root with that exact delete walker never descends into (and so never removes) an entry whose
name is left untouched (used to protect the --delay-updates staging relative path equals one of the skip_prefixes or lies below one: used to
directory, which holds files that are still to be published). */ protect the --delay-updates staging directory (files still to be published)
and the --compare-dest/--copy-dest/--link-dest basis trees (snapshots the
transfer links from, never destination content). */
bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete, bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete,
const char* skip_root_child); const char* const* skip_prefixes, int skip_prefix_count);
bool utils_set_authorized_root(int fd, const char* canonical_path); bool utils_set_authorized_root(int fd, const char* canonical_path);
/* The fd-only compatibility form is fail-closed for path-based operations; /* The fd-only compatibility form is fail-closed for path-based operations;
* callers should use utils_set_authorized_root with the canonical identity. */ * callers should use utils_set_authorized_root with the canonical identity. */