feat(xattr): rsync-interoperable --fake-super stat; fix --devices error parity

This commit is contained in:
2026-09-22 22:03:41 +02:00
parent d780a4625e
commit 80e8d7f450
11 changed files with 315 additions and 149 deletions
+35 -33
View File
@@ -363,16 +363,21 @@ bool xattr_apply_fd(int fd, const FileXattrList* list) {
return true;
}
/* ---- --fake-super: park ownership/mode/mtime in a reserved xattr ---- */
/* ---- --fake-super: park ownership/mode/rdev in a reserved xattr ---- */
void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, int64_t mtime_sec,
int64_t mtime_nsec) {
void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, uint32_t rdev_major,
uint32_t rdev_minor) {
if (fd < 0)
return;
char record[128];
int len =
snprintf(record, sizeof(record), "%lu:%lu:%03o:%lld:%ld", (unsigned long)uid,
(unsigned long)gid, (unsigned)mode & 0777U, (long long)mtime_sec, (long)mtime_nsec);
/* rsync 3.4.1's exact grammar: "<octal full st_mode> <rdev_major>,<rdev_minor>
* <uid>:<gid>". The octal mode carries the S_IFMT bits (e.g. 0104711 for a
* setuid regular file, 020644 for a char device); the rdev pair is 0,0 for a
* non-device. No mtime field: rsync leaves the file's own timestamp in
* charge of mtime. This value is what rsync reads back to restore a
* fake-super tree, so the field order and separators must not change. */
char record[96];
int len = snprintf(record, sizeof(record), "%o %u,%u %u:%u", (unsigned)mode, (unsigned)rdev_major,
(unsigned)rdev_minor, (unsigned)uid, (unsigned)gid);
if (len <= 0 || (size_t)len >= sizeof(record))
return;
if (fsetxattr(fd, FAKESUPER_XATTR, record, (size_t)len, 0) != 0) {
@@ -381,11 +386,14 @@ void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, int6
}
}
/* --fake-super replay: read the freshly-stored record and re-apply mode/mtime
* fd-relative. The recorded uid/gid are retained for a later privileged
* restore but are NEVER chowned here: --fake-super only RECORDS ownership, it
* must not real-chown the recorded (resolved) owner. Mode/mtime still apply so
* unprivileged --fake-super keeps working. */
/* --fake-super replay: read the freshly-stored record and re-apply its
* permission bits fd-relative. The recorded uid/gid are retained for a later
* privileged restore but are NEVER chowned here: --fake-super only RECORDS
* ownership, it must not real-chown the recorded (resolved) owner. The
* recorded rdev is likewise parsed for grammar compatibility but is not acted
* on (device recreation is a separate, privilege-gated path). mtime is not in
* the record: the normal metadata path applies it (policy.times), exactly as
* rsync relies on the file's own timestamp. */
bool fake_super_restore_fd(int fd, FileAttrPolicy policy) {
if (fd < 0)
return false;
@@ -394,24 +402,25 @@ bool fake_super_restore_fd(int fd, FileAttrPolicy policy) {
if (len < 0)
return false; /* absent or filesystem without xattrs: silent no-op */
record[len] = '\0';
unsigned long ul_uid, ul_gid, ul_mode;
long long mtime_sec;
long mtime_nsec;
if (sscanf(record, "%lu:%lu:%lo:%lld:%ld", &ul_uid, &ul_gid, &ul_mode, &mtime_sec, &mtime_nsec) !=
5)
unsigned ul_mode, rdev_major, rdev_minor, ul_uid, ul_gid;
if (sscanf(record, "%o %u,%u %u:%u", &ul_mode, &rdev_major, &rdev_minor, &ul_uid, &ul_gid) != 5)
return false; /* malformed record: skip, never fatal */
/* --fake-super NEVER performs a real chown: that would defeat the whole
point of the flag (record privileged ownership on an unprivileged receiver
for a later privileged restore). The uid/gid parsed above are retained in
the record for that later restore, but no ownership change happens here. */
/* --fake-super NEVER performs a real chown: that would defeat the whole point
of the flag (record privileged ownership on an unprivileged receiver for a
later privileged restore). The uid/gid parsed above are retained in the
record for that later restore, but no ownership change happens here. The
rdev is retained for the same reason. */
(void)rdev_major;
(void)rdev_minor;
(void)ul_uid;
(void)ul_gid;
/* Mode is applied only when the per-attribute policy asks for it, through the
SAME shared helper the normal metadata path uses (metadata_mode_for_policy):
under --perms the recorded source mode is copied exactly, including
group/other write and setuid/setgid/sticky bits (rsync parity), and the -E
rule derives exec bits from the destination's read bits exactly like
SAME shared helper the normal metadata path uses (metadata_mode_for_policy).
The recorded special bits are stripped first: rsync's fake-super receiver
stores the full mode in the xattr but never installs setuid/setgid/sticky on
the real file, so only the 0777 permission bits may be replayed. The -E
rule then derives exec bits from the destination's read bits exactly like
file_restore_metadata_fd. */
if (policy.perms || policy.executability) {
struct stat cur;
@@ -419,19 +428,12 @@ bool fake_super_restore_fd(int fd, FileAttrPolicy policy) {
if (fstat(fd, &cur) != 0) {
log_message(LOG_LEVEL_WARNING, "--fake-super: could not read destination mode: %s",
strerror(errno));
} else if (metadata_mode_for_policy((mode_t)ul_mode, cur.st_mode, policy, &want)) {
} 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",
strerror(errno));
}
}
if (policy.times) {
struct timespec times[2] = {{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
{.tv_sec = (time_t)mtime_sec, .tv_nsec = mtime_nsec}};
if (futimens(fd, times) != 0)
log_message(LOG_LEVEL_WARNING,
"--fake-super: could not restore mtime on destination file: %s", strerror(errno));
}
return true;
}