feat(xattr): --fake-super for directories (#319)

This commit is contained in:
2026-09-24 02:30:02 +02:00
parent a14fbb2c10
commit cc931790e9
7 changed files with 279 additions and 50 deletions
+64 -34
View File
@@ -102,12 +102,14 @@ bool dir_metadata_should_capture(const Config* config) {
/* Directory metadata is captured when a directory attribute is actually
* 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). */
* -X/-A (directory xattrs/ACLs), or --fake-super (whose reserved %stat record
* is written on the directory itself, so its metadata must travel).
* --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_owner || config->preserve_group || config->preserve_xattrs ||
config->preserve_acls);
config->preserve_acls || config->fake_super);
}
void dir_time_list_init(DirTimeList* list) {
@@ -205,12 +207,19 @@ void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory
bool apply_times = config->preserve_times && !config->omit_dir_times;
bool apply_mode = config->preserve_perms;
bool apply_xattrs = config->use_xattrs;
bool apply_fake_super = config->fake_super;
/* 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();
* unless an ownership request is active), xattrs only when -X/-A was
* negotiated, and the --fake-super record whenever the flag is active.
* Times/mode keep their own per-attribute gates. */
bool have_any =
apply_times || apply_mode || apply_xattrs || apply_fake_super || identity_active_enabled();
if (!have_any)
return;
/* Built once: the --fake-super replay uses it to apply only the recorded
* permission bits (the special bits stay in the record, exactly like the
* regular-file fake-super receiver). */
FileAttrPolicy policy = file_attr_policy_from_config(config);
for (size_t i = 0; i < list->count; i++) {
char* dir_path = path_cat(root_directory, list->paths[i]);
if (!dir_path)
@@ -260,38 +269,59 @@ void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory
free(escaped_path);
}
}
if (apply_mode) {
mode_t dir_mode = list->entries[i].mode;
bool mode_ready = true;
if (config->chmod_spec && *config->chmod_spec &&
!chmod_apply(dir_mode, config->chmod_spec, &dir_mode)) {
/* The final directory mode (after any --chmod) is computed once so the
--fake-super record can carry it even when the on-disk replay is
restricted to the permission bits below. */
mode_t dir_mode = list->entries[i].mode;
bool mode_ready = true;
if (apply_mode && config->chmod_spec && *config->chmod_spec &&
!chmod_apply(dir_mode, config->chmod_spec, &dir_mode)) {
char* escaped_path = output_escape(dir_path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING, "Failed to apply --chmod to directory %s",
escaped_path ? escaped_path : "<allocation failed>");
free(escaped_path);
mode_ready = false;
}
/* Under --fake-super the normal fchmod below still applies the mode, but
the fake-super replay that follows narrows the on-disk result to the
recorded permission bits (the full mode, including setuid/setgid/sticky,
lives only in the record). Keeping the normal fchmod first means a
filesystem without xattr support still gets the directory mode rather than
silently losing it. */
if (apply_mode && mode_ready) {
/* rsync -p copies the source directory mode exactly, including
* group/other write and the setgid/sticky bits. Setuid/setgid/sticky
* are super-user activities: when the connection forbade them
* (SUPER_MODE_OFF / --no-super), strip them even under -p. */
mode_t safe_mode = dir_mode & (mode_t)(S_ISUID | S_ISGID | S_ISVTX | 0777);
if (!privilege_super_mode_permitted(config->super_mode))
safe_mode &= ~(mode_t)(S_ISUID | S_ISGID | S_ISVTX);
if (dir_fd < 0) {
char* escaped_path = output_escape(dir_path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING, "Failed to apply --chmod to directory %s",
escaped_path ? escaped_path : "<allocation failed>");
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);
mode_ready = false;
}
if (mode_ready) {
/* rsync -p copies the source directory mode exactly, including
* group/other write and the setgid/sticky bits. Setuid/setgid/sticky
* are super-user activities: when the connection forbade them
* (SUPER_MODE_OFF / --no-super), strip them even under -p. */
mode_t safe_mode = dir_mode & (mode_t)(S_ISUID | S_ISGID | S_ISVTX | 0777);
if (!privilege_super_mode_permitted(config->super_mode))
safe_mode &= ~(mode_t)(S_ISUID | S_ISGID | S_ISVTX);
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);
}
}
}
/* --fake-super: park the directory's full stat (rsync 3.4.1's exact
grammar) on the directory ITSELF, then replay only the recorded
permission bits fd-relative. The special bits live only in the record
and the recorded ownership is never real-chowned: the resolved ids are
stored for a later privileged restore, exactly like the file path. Runs
before the xattr apply so a mode change cannot clobber the ACL mask. */
if (apply_fake_super && dir_fd >= 0) {
uint32_t store_uid = 0;
uint32_t store_gid = 0;
identity_resolve_storage_ids((int32_t)list->entries[i].uid, (int32_t)list->entries[i].gid,
&store_uid, &store_gid);
fake_super_store_fd(dir_fd, store_uid, store_gid, (uint32_t)dir_mode, 0, 0);
fake_super_restore_fd(dir_fd, policy);
}
/* 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)
+15
View File
@@ -817,6 +817,21 @@ static FileSaveResult file_save_directory_to_disk(const FileSavePlan* plan, bool
} else if (ok && identity_copy_as_active()) {
ok = false;
}
/* --fake-super: park the directory's full stat in rsync's reserved
user.rsync.%stat xattr as soon as the directory exists. This makes even a
direct file_save_to_disk_full() caller -- which never runs the deferred
DirTimeList pass -- produce an rsync-readable fake-super record. The record
carries the full mode/uid/gid; the permission bits are replayed by the
deferred pass (never inline, so a restrictive mode cannot block child
creation) and the recorded ownership is never real-chowned. Best-effort:
fake_super_store_fd() logs and skips a failure, never failing the entry. */
if (ok && plan->config && plan->config->fake_super && file->metadata && dir_fd >= 0) {
uint32_t store_uid = 0;
uint32_t store_gid = 0;
identity_resolve_storage_ids((int32_t)file->metadata->uid, (int32_t)file->metadata->gid,
&store_uid, &store_gid);
fake_super_store_fd(dir_fd, store_uid, store_gid, (uint32_t)file->metadata->mode, 0, 0);
}
/* The final source MODE is deliberately NOT applied inline. A restrictive
source mode (for example 0555) would make the directory unwritable before
its children are created, so a non-root receiver fails each child with
+2 -2
View File
@@ -450,7 +450,7 @@ void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, uint
if (len <= 0 || (size_t)len >= sizeof(record))
return;
if (fsetxattr(fd, FAKESUPER_XATTR, record, (size_t)len, 0) != 0) {
log_message(LOG_LEVEL_WARNING, "--fake-super: could not store %s on destination file: %s",
log_message(LOG_LEVEL_WARNING, "--fake-super: could not store %s on destination entry: %s",
FAKESUPER_XATTR, strerror(errno));
}
}
@@ -550,7 +550,7 @@ bool fake_super_restore_fd(int fd, FileAttrPolicy policy) {
} else if (metadata_mode_for_policy((mode_t)(ul_mode & 0777U), cur.st_mode, policy, &want)) {
if (fchmod(fd, want) != 0)
log_message(LOG_LEVEL_WARNING,
"--fake-super: could not restore mode on destination file: %s",
"--fake-super: could not restore mode on destination entry: %s",
strerror(errno));
}
}
+10 -6
View File
@@ -140,21 +140,25 @@ bool xattr_apply_path_nofollow(int parent_fd, const char* leaf, const FileXattrL
/* --fake-super: write the source uid/gid/mode/rdev record into the reserved
* FAKESUPER_XATTR on `fd`, using rsync 3.4.1's exact grammar (see the key
* comment above). `mode` is the full st_mode including its S_IFMT bits.
* Best-effort (logged, never fatal). Only meaningful when metadata was
* transmitted so the values exist. */
* `fd` may be a regular file, a faked char/block device (written as a regular
* file), or a DIRECTORY: rsync stores a directory's faked mode/uid/gid in the
* reserved xattr on the directory itself. Best-effort (logged, never fatal).
* Only meaningful when metadata was transmitted so the values exist. */
void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, uint32_t rdev_major,
uint32_t rdev_minor);
/* --fake-super replay: parse the FAKESUPER_XATTR record previously written on
* `fd` by fake_super_store_fd and re-apply the recorded permission bits
* fd-relative. The recorded uid/gid are deliberately NOT chowned for real:
* --fake-super only RECORDS ownership (the caller stores the resolved mapping
* via identity_resolve_storage_ids), it never performs a real chown. The
* fd-relative. `fd` may be a regular file, a faked device, or a DIRECTORY;
* fgetxattr/fchmod work identically on a directory descriptor. The recorded
* uid/gid are deliberately NOT chowned for real: --fake-super only RECORDS
* ownership (the caller stores the resolved mapping via
* identity_resolve_storage_ids), it never performs a real chown. The
* recorded rdev is retained for a later privileged restore but is not acted on
* here. Best-effort: absence of the xattr or a malformed record is a silent
* no-op that never fails the transfer. The MODE leg is applied only when
* policy.perms||policy.executability, and the recorded special bits
* (setuid/setgid/sticky) are NOT applied to the real file -- exactly like
* (setuid/setgid/sticky) are NOT applied to the real entry -- exactly like
* rsync's fake-super receiver, which stores the full mode in the xattr but
* strips the special bits on disk. mtime is not part of the record; the normal
* metadata path carries it (policy.times) exactly as rsync sets the file's own