fix(identity): rsync ownership parity for numeric-ids, dirs, maps, fake-super (#286, #294)

- #286: --numeric-ids is a mapping modifier only; it no longer activates
  chown by itself (identity_active_enabled/owner/group predicates), and
  --fake-super stores the resolved mapping instead of real-chowning.
- #286: apply owner/group to directories via the deferred directory
  metadata path; capture+transmit+apply directory xattrs/ACLs (-aX/-aA),
  including default ACLs, in STATUS_MKDIR/STATUS_DIR_TIMES.
- #294: --usermap/--groupmap support inclusive ranges, '*', empty FROM
  (unnamed ids), and receiver-side TO name resolution; --chown mixing with
  a same-side map is rejected like rsync.
- Protocol 2.22.0 -> 2.23.0 (map wire entry gains from_hi + to_name;
  dir frames gain a bounded xattr block).
This commit is contained in:
2026-09-15 22:10:02 +02:00
parent 23552e823d
commit 6144c7fc7f
26 changed files with 1115 additions and 376 deletions
+72 -26
View File
@@ -2421,11 +2421,14 @@ File* file_receive(const Config* config, int file_descriptor) {
bool dir_metadata_should_capture(const Config* config) {
/* Directory metadata is captured when a directory attribute is actually
* requested: -p/--perms (directory modes) or -t/--times (directory mtimes,
* unless -O/--omit-dir-times suppresses them). --atimes/-U alone does not
* pull directory metadata (matching the original dir-time bundle). */
* requested: -p/--perms (directory modes), -t/--times (directory mtimes,
* unless -O/--omit-dir-times suppresses them), -o/-g (directory ownership),
* or -X/-A (directory xattrs/ACLs). --atimes/-U alone does not pull
* directory metadata (matching the original dir-time bundle). */
return config && config->use_metadata &&
(config->preserve_perms || (config->preserve_times && !config->omit_dir_times));
(config->preserve_perms || (config->preserve_times && !config->omit_dir_times) ||
config->preserve_owner || config->preserve_group || config->preserve_xattrs ||
config->preserve_acls);
}
void dir_time_list_init(DirTimeList* list) {
@@ -2433,6 +2436,7 @@ void dir_time_list_init(DirTimeList* list) {
return;
list->paths = NULL;
list->entries = NULL;
list->xattrs = NULL;
list->count = 0;
list->capacity = 0;
list->bytes = 0;
@@ -2441,18 +2445,23 @@ void dir_time_list_init(DirTimeList* list) {
void dir_time_list_free(DirTimeList* list) {
if (!list)
return;
for (size_t i = 0; i < list->count; i++)
for (size_t i = 0; i < list->count; i++) {
free(list->paths[i]);
xattr_list_free(list->xattrs ? list->xattrs[i] : NULL);
}
free(list->paths);
free(list->entries);
free(list->xattrs);
list->paths = NULL;
list->entries = NULL;
list->xattrs = NULL;
list->count = 0;
list->capacity = 0;
list->bytes = 0;
}
bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetadata* metadata) {
bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetadata* metadata,
const FileXattrList* xattrs) {
if (!list || !wire_path || !metadata)
return true; /* nothing to remember; never a hard error */
/* Cumulative, not per-frame: the sender may stream a tree across unbounded
@@ -2461,9 +2470,14 @@ bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetad
transfer, which becomes a clean protocol error). */
size_t path_len = strlen(wire_path);
/* Charge the whole per-entry cost (path copy + pointer slot + metadata
struct), not just the path, so the array growth is bounded by the same
cumulative budget. */
size_t entry_cost = path_len + sizeof(FileMetadata) + sizeof(char*);
struct + captured xattrs), not just the path, so the array growth is
bounded by the same cumulative budget. */
size_t xattr_cost = 0;
if (xattrs) {
for (int i = 0; i < xattrs->count; i++)
xattr_cost += strlen(xattrs->items[i].name) + xattrs->items[i].value_len + sizeof(FileXattr);
}
size_t entry_cost = path_len + sizeof(FileMetadata) + 2 * sizeof(char*) + xattr_cost;
if (list->count >= MAX_DIR_TIME_ENTRIES || entry_cost > MAX_DIR_TIME_BYTES - list->bytes)
return false;
if (list->count == list->capacity) {
@@ -2472,10 +2486,9 @@ bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetad
return false;
/* Assign each grown array as soon as its realloc succeeds: the old block is
already freed by then, so discarding the pointer would dangle. capacity
is advanced only after BOTH reallocs succeed, so a partial failure leaves
capacity no larger than the entries allocation (the paths array may be
over-allocated, which is harmless) -- never a mismatched list the next
add could write past. */
is advanced only after ALL reallocs succeed, so a partial failure leaves
capacity no larger than the smallest allocation -- never a mismatched
list the next add could write past. */
char** grown_paths = realloc(list->paths, new_capacity * sizeof(char*));
if (!grown_paths)
return false;
@@ -2484,13 +2497,23 @@ bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetad
if (!grown_entries)
return false;
list->entries = grown_entries;
FileXattrList** grown_xattrs = realloc(list->xattrs, new_capacity * sizeof(FileXattrList*));
if (!grown_xattrs)
return false;
list->xattrs = grown_xattrs;
list->capacity = new_capacity;
}
char* copy = str_dup(wire_path);
if (!copy)
return false;
FileXattrList* xattr_copy = xattr_list_clone(xattrs);
if (xattrs && !xattr_copy) {
free(copy);
return false;
}
list->paths[list->count] = copy;
list->entries[list->count] = *metadata;
list->xattrs[list->count] = xattr_copy;
list->count++;
list->bytes += entry_cost;
return true;
@@ -2502,7 +2525,12 @@ void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory
return;
bool apply_times = config->preserve_times && !config->omit_dir_times;
bool apply_mode = config->preserve_perms;
if (!apply_times && !apply_mode)
bool apply_xattrs = config->use_xattrs;
/* Ownership is applied through the active identity snapshot (which no-ops
* unless an ownership request is active), and xattrs only when -X/-A was
* negotiated. Times/mode keep their own per-attribute gates. */
bool have_any = apply_times || apply_mode || apply_xattrs || identity_active_enabled();
if (!have_any)
return;
for (size_t i = 0; i < list->count; i++) {
char* dir_path = path_cat(root_directory, list->paths[i]);
@@ -2531,6 +2559,13 @@ void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory
free(dir_path);
continue;
}
/* One O_DIRECTORY|O_NOFOLLOW fd drives ownership/mode/xattr application so
none of them can follow a same-named symlink planted after the fstatat. */
int dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
/* Ownership first: a chown clears setuid/setgid, so it must precede mode. */
if (dir_fd >= 0)
identity_apply_ownership(dir_fd, (int32_t)list->entries[i].uid,
(int32_t)list->entries[i].gid);
if (apply_times) {
struct timespec times[2] = {
{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
@@ -2560,28 +2595,28 @@ void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory
if (mode_ready) {
/* Route the directory mode through the SAME sanitization as the
* regular-file policy: a client-supplied mode never grants group/other
* write. Open the directory with O_DIRECTORY|O_NOFOLLOW (never
* following a same-named symlink) and fchmod the fd, avoiding the
* fchmodat(..., 0) TOCTOU/symlink-follow hole. */
* write. */
mode_t safe_mode =
(dir_mode & 0777 & ~(S_IWGRP | S_IWOTH)) | (dir_mode & (S_ISGID | S_ISVTX));
int dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (dir_fd < 0) {
char* escaped_path = output_escape(dir_path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING, "Failed to open directory %s to set its mode: %s",
escaped_path ? escaped_path : "<allocation failed>", strerror(errno));
free(escaped_path);
} else {
if (fchmod(dir_fd, safe_mode) != 0) {
char* escaped_path = output_escape(dir_path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING, "Failed to set directory mode on %s: %s",
escaped_path ? escaped_path : "<allocation failed>", strerror(errno));
free(escaped_path);
}
close(dir_fd);
} else if (fchmod(dir_fd, safe_mode) != 0) {
char* escaped_path = output_escape(dir_path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING, "Failed to set directory mode on %s: %s",
escaped_path ? escaped_path : "<allocation failed>", strerror(errno));
free(escaped_path);
}
}
}
/* xattrs/ACLs last: a mode change can rewrite the ACL mask, so the ACL
xattrs must be (re)applied after fchmod. */
if (apply_xattrs && dir_fd >= 0 && list->xattrs)
xattr_apply_fd(dir_fd, list->xattrs[i]);
if (dir_fd >= 0)
close(dir_fd);
close(parent_fd);
free(leaf);
free(dir_path);
@@ -2621,6 +2656,11 @@ File* file_receive_directory(int file_descriptor, const Config* config) {
return NULL;
}
}
/* Directory xattrs/ACLs (-X/-A) ride after the metadata when negotiated. */
if (config && !receive_file_xattrs(file, file_descriptor, config)) {
file_destroy(file);
return NULL;
}
return file;
}
@@ -2657,6 +2697,12 @@ File* file_receive_dir_time(int file_descriptor, const Config* config) {
return NULL;
}
}
/* Directory xattrs/ACLs (-X/-A) ride after the metadata, mirroring the
sender's send_dir_times(). */
if (config && !receive_file_xattrs(file, file_descriptor, config)) {
file_destroy(file);
return NULL;
}
return file;
}