fix(xattr): fake-super device round-trip, --devices continue-on-error, harden stat parse

This commit is contained in:
2026-09-22 23:05:40 +02:00
parent 0b40c47d6a
commit 47b1b9b915
12 changed files with 310 additions and 47 deletions
+27 -21
View File
@@ -1182,7 +1182,8 @@ int file_open_temp_dir(const char* dir_path) {
* destination file) and best-effort: a per-attribute or privilege failure is
* logged and skipped, never fatal. */
static void restore_extra_fd(int fd, const FileMetadata* metadata, const FileXattrList* xattrs,
bool fake_super, FileAttrPolicy policy) {
bool fake_super, FileAttrPolicy policy, uint32_t fake_super_rdev_major,
uint32_t fake_super_rdev_minor) {
xattr_apply_fd(fd, xattrs);
if (fake_super && metadata) {
/* Record the ownership that WOULD have been applied: when an explicit
@@ -1197,7 +1198,8 @@ static void restore_extra_fd(int fd, const FileMetadata* metadata, const FileXat
uint32_t store_gid;
identity_resolve_storage_ids((int32_t)metadata->uid, (int32_t)metadata->gid, &store_uid,
&store_gid);
fake_super_store_fd(fd, store_uid, store_gid, (uint32_t)metadata->mode, 0, 0);
fake_super_store_fd(fd, store_uid, store_gid, (uint32_t)metadata->mode, fake_super_rdev_major,
fake_super_rdev_minor);
fake_super_restore_fd(fd, policy);
}
}
@@ -1207,7 +1209,8 @@ file_to_disk_secure_impl(const char* path, const void* data, unsigned long long
bool inplace, bool sparse, bool preallocate, const FileMetadata* metadata,
FileAttrPolicy policy, bool update, bool no_replace, bool use_fsync,
const char* temp_dir, const FileXattrList* xattrs, bool fake_super,
bool keep_partial, unsigned* dirs_created, const char* count_floor) {
bool keep_partial, unsigned* dirs_created, const char* count_floor,
uint32_t fake_super_rdev_major, uint32_t fake_super_rdev_minor) {
char* leaf = NULL;
int dirfd = file_open_secure_parent_counted(path, &leaf, true, dirs_created, count_floor);
if (dirfd < 0)
@@ -1314,7 +1317,8 @@ file_to_disk_secure_impl(const char* path, const void* data, unsigned long long
}
}
if (ok)
restore_extra_fd(fd, metadata, xattrs, fake_super, policy);
restore_extra_fd(fd, metadata, xattrs, fake_super, policy, fake_super_rdev_major,
fake_super_rdev_minor);
if (ok && use_fsync)
ok = fsync(fd) == 0;
}
@@ -1432,7 +1436,8 @@ file_to_disk_secure_impl(const char* path, const void* data, unsigned long long
}
}
if (ok)
restore_extra_fd(fd, metadata, xattrs, fake_super, policy);
restore_extra_fd(fd, metadata, xattrs, fake_super, policy, fake_super_rdev_major,
fake_super_rdev_minor);
if (ok && use_fsync)
ok = fsync(fd) == 0;
}
@@ -1500,7 +1505,8 @@ file_to_disk_secure_impl(const char* path, const void* data, unsigned long long
"non-atomic copy into the destination directory");
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
policy, update, no_replace, use_fsync, NULL, xattrs, fake_super,
keep_partial, dirs_created, count_floor);
keep_partial, dirs_created, count_floor, fake_super_rdev_major,
fake_super_rdev_minor);
}
return ok;
}
@@ -1510,7 +1516,7 @@ bool file_to_disk_secure(const char* path, const void* data, unsigned long long
FileAttrPolicy policy, const char* temp_dir) {
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
policy, false, false, false, temp_dir, NULL, false, false, NULL,
NULL);
NULL, 0, 0);
}
bool file_to_disk_secure_update(const char* path, const void* data, unsigned long long data_size,
@@ -1519,7 +1525,7 @@ bool file_to_disk_secure_update(const char* path, const void* data, unsigned lon
const char* temp_dir) {
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
policy, true, false, false, temp_dir, NULL, false, false, NULL,
NULL);
NULL, 0, 0);
}
bool file_to_disk_secure_with_fsync(const char* path, const void* data,
@@ -1528,7 +1534,7 @@ bool file_to_disk_secure_with_fsync(const char* path, const void* data,
FileAttrPolicy policy, bool use_fsync, const char* temp_dir) {
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
policy, false, false, use_fsync, temp_dir, NULL, false, false,
NULL, NULL);
NULL, NULL, 0, 0);
}
bool file_to_disk_secure_no_replace(const char* path, const void* data,
@@ -1537,7 +1543,7 @@ bool file_to_disk_secure_no_replace(const char* path, const void* data,
const char* temp_dir) {
return file_to_disk_secure_impl(path, data, data_size, false, sparse, preallocate, metadata,
policy, false, true, false, temp_dir, NULL, false, false, NULL,
NULL);
NULL, 0, 0);
}
/* Receiver write-path variant that also applies the per-file xattrs (-X/-A)
@@ -1552,19 +1558,19 @@ bool file_to_disk_secure_attrs(const char* path, const void* data, unsigned long
bool fake_super, bool keep_partial, const char* temp_dir) {
return file_to_disk_secure_attrs_counted(path, data, data_size, inplace, sparse, preallocate,
metadata, policy, update, no_replace, use_fsync, xattrs,
fake_super, keep_partial, temp_dir, NULL, NULL);
fake_super, keep_partial, temp_dir, NULL, NULL, 0, 0);
}
bool file_to_disk_secure_attrs_counted(const char* path, const void* data,
unsigned long long data_size, bool inplace, bool sparse,
bool preallocate, const FileMetadata* metadata,
FileAttrPolicy policy, bool update, bool no_replace,
bool use_fsync, const FileXattrList* xattrs, bool fake_super,
bool keep_partial, const char* temp_dir,
unsigned* dirs_created, const char* count_floor) {
bool file_to_disk_secure_attrs_counted(
const char* path, const void* data, unsigned long long data_size, bool inplace, bool sparse,
bool preallocate, const FileMetadata* metadata, FileAttrPolicy policy, bool update,
bool no_replace, bool use_fsync, const FileXattrList* xattrs, bool fake_super,
bool keep_partial, const char* temp_dir, unsigned* dirs_created, const char* count_floor,
uint32_t fake_super_rdev_major, uint32_t fake_super_rdev_minor) {
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
policy, update, no_replace, use_fsync, temp_dir, xattrs,
fake_super, keep_partial, dirs_created, count_floor);
fake_super, keep_partial, dirs_created, count_floor,
fake_super_rdev_major, fake_super_rdev_minor);
}
/* Atomic --link-dest install. The destination is replaced (via a temporary
@@ -1694,7 +1700,7 @@ static bool file_copy_basis_stream_impl(const char* path, const char* basis_path
wrote = false;
}
if (wrote)
restore_extra_fd(fd, metadata, xattrs, fake_super, policy);
restore_extra_fd(fd, metadata, xattrs, fake_super, policy, 0, 0);
if (wrote && use_fsync)
wrote = fsync(fd) == 0;
if (close(fd) != 0)
@@ -1843,7 +1849,7 @@ static bool file_to_disk_secure_link_impl(const char* path, const char* basis_pa
return true;
return file_to_disk_secure_attrs_counted(
path, data, data_size, false, false, preallocate, metadata, policy, false, false, use_fsync,
xattrs, fake_super, false, temp_dir, dirs_created, count_floor);
xattrs, fake_super, false, temp_dir, dirs_created, count_floor, 0, 0);
}
if (scratch_dirfd >= 0)
+6 -7
View File
@@ -182,13 +182,12 @@ bool file_copy_basis_stream_attrs(const char* path, const char* basis_path,
* confined secure walk had to create that lie strictly below `count_floor` (a
* receive-root-relative prefix, or NULL for all). Used to reproduce rsync's
* `Number of created files` directory count on a fresh destination. */
bool file_to_disk_secure_attrs_counted(const char* path, const void* data,
unsigned long long data_size, bool inplace, bool sparse,
bool preallocate, const FileMetadata* metadata,
FileAttrPolicy policy, bool update, bool no_replace,
bool use_fsync, const FileXattrList* xattrs, bool fake_super,
bool keep_partial, const char* temp_dir,
unsigned* dirs_created, const char* count_floor);
bool file_to_disk_secure_attrs_counted(
const char* path, const void* data, unsigned long long data_size, bool inplace, bool sparse,
bool preallocate, const FileMetadata* metadata, FileAttrPolicy policy, bool update,
bool no_replace, bool use_fsync, const FileXattrList* xattrs, bool fake_super,
bool keep_partial, const char* temp_dir, unsigned* dirs_created, const char* count_floor,
uint32_t fake_super_rdev_major, uint32_t fake_super_rdev_minor);
bool file_to_disk_secure_link_attrs_counted(const char* path, const char* basis_path,
const void* data, unsigned long long data_size,
bool preallocate, const FileMetadata* metadata,
+21 -12
View File
@@ -401,12 +401,13 @@ bool file_special_rdev_valid(int32_t major, int32_t minor, mode_t mode) {
*
* Privilege gating: making a real device node requires CAP_MKNOD (root); making
* a FIFO works unprivileged (mkfifo). A device node whose mknodat() fails with
* EPERM/EACCES is a genuine transfer error (rsync parity: rsync reports the
* mknod failure and the run exits partial, code 23). Only the unprivileged
* FIFO/socket (--specials) path keeps the best-effort skip, because those are
* normally creatable without privilege and a failure there is environmental.
* CI runs non-root, so device creation is expected to fail there; only a FIFO
* is honestly assertable unprivileged.
* EPERM/EACCES is a PER-ENTRY failure (rsync parity: rsync reports the mknod
* failure, still transfers the rest, and exits partial, code 23), reported as
* FILE_SAVE_FAILED so the receiver counts it and continues. Only the
* unprivileged FIFO/socket (--specials) path keeps the best-effort skip,
* because those are normally creatable without privilege and a failure there is
* environmental. CI runs non-root, so device creation is expected to fail
* there; only a FIFO is honestly assertable unprivileged.
*
* Confinement: the parent directory is opened fd-relative below the receive
* root (file_open_secure_parent: O_NOFOLLOW, no "..", root-checked) and the
@@ -548,10 +549,10 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
const char* shown_path = escaped_path ? escaped_path : "<allocation failed>";
if (is_char || is_blk) {
/* rsync parity: a device node that cannot be created (no CAP_MKNOD, or
* super-user activities not permitted) is a genuine transfer error.
* rsync reports `mknod ".../node" failed: ...` and the run exits
* partial (23); FastSync surfaces it through the outcome aggregation
* instead of silently skipping the entry. FIFO/socket creation
* super-user activities not permitted) is a per-entry failure. rsync
* logs `mknod ".../node" failed: ...`, still transfers the remaining
* files, and exits partial (23); FastSync logs it, counts it, and
* continues rather than aborting the stream. FIFO/socket creation
* (--specials) keeps the best-effort skip path below. */
log_message(LOG_LEVEL_ERROR,
"cannot create %s %s: %s\n"
@@ -561,7 +562,7 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
close(parent_fd);
free(leaf);
free(destination);
return FILE_SAVE_ERROR;
return FILE_SAVE_FAILED;
}
/* Missing CAP_MKNOD / parent write permission for a FIFO/socket: the
environment cannot create the node, so skip instead of failing the
@@ -936,6 +937,14 @@ static bool file_save_try_special_dispatch(const FileSavePlan* plan, bool* creat
/* Device/special node (--devices/--specials): recreate the node instead of
writing content (privilege-gated, confined, rdev-validated). */
if (file->is_special) {
/* Under --fake-super rsync never mknod()s a device: it writes a regular
empty file and records the real rdev in user.rsync.%stat. Fall through to
the ordinary writer so the device round-trips (its S_IFMT mode bits and
rdev are parked in the record). Without --fake-super the node is
recreated (or, when privilege is refused, handled per-entry). */
mode_t special_mode = file->metadata ? file->metadata->mode : 0;
if (config && config->fake_super && (S_ISCHR(special_mode) || S_ISBLK(special_mode)))
return false;
*out = file_save_special_to_disk(plan->root_directory, file, config, created);
return true;
}
@@ -1110,7 +1119,7 @@ static bool file_save_install_data(FileSavePlan* plan, const FileMetadata* metad
config && config->preallocate, metadata, plan->policy, config && config->update,
config && config->ignore_existing, config && config->use_fsync, file->xattrs,
config ? config->fake_super : false, config ? config->partial : false, plan->confined_temp,
created_dirs, count_floor);
created_dirs, count_floor, (uint32_t)file->rdev_major, (uint32_t)file->rdev_minor);
}
free(count_floor);
return ok;
+10 -2
View File
@@ -12,8 +12,16 @@
/* Outcome of a single file_save_to_disk operation. The receiver needs to
distinguish "written" from "skipped" so --remove-source-files can be told
which sources were actually stored. */
typedef enum { FILE_SAVE_ERROR = 0, FILE_SAVE_WRITTEN = 1, FILE_SAVE_SKIPPED = 2 } FileSaveResult;
which sources were actually stored. FILE_SAVE_FAILED is a per-entry failure
(for example a device node that mknodat() refused with EPERM/EACCES): it is
logged and counted by the receiver but does NOT abort the transfer, matching
rsync's continue-and-exit-partial behavior. */
typedef enum {
FILE_SAVE_ERROR = 0,
FILE_SAVE_WRITTEN = 1,
FILE_SAVE_SKIPPED = 2,
FILE_SAVE_FAILED = 3
} FileSaveResult;
bool file_special_rdev_valid(int32_t major, int32_t minor, mode_t mode);
+52 -1
View File
@@ -7,6 +7,7 @@
#include "utils.h"
#include "file_types.h"
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -386,6 +387,56 @@ void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, uint
}
}
/* Parse rsync's `user.rsync.%stat` grammar strictly:
* "<octal st_mode> <rdev_major>,<rdev_minor> <uid>:<gid>"
* Every field is parsed with strtoul() so an out-of-range value is a clean
* rejection rather than the undefined behavior sscanf("%u") exhibited, each
* field is range-checked against the same bounds the wire validator uses, and
* the whole record must be consumed (only trailing whitespace is tolerated) so
* trailing garbage is refused. Returns false on any malformed input. */
static bool fake_super_parse_stat(const char* record, unsigned* mode_out, unsigned* rdev_major_out,
unsigned* rdev_minor_out, unsigned* uid_out, unsigned* gid_out) {
if (!record)
return false;
char* end = NULL;
const char* p = record;
errno = 0;
unsigned long mode = strtoul(p, &end, 8);
if (errno != 0 || end == p || mode > (unsigned long)UINT_MAX || *end != ' ')
return false;
p = end + 1;
errno = 0;
unsigned long rdev_major = strtoul(p, &end, 10);
if (errno != 0 || end == p || rdev_major > 0xffffUL || *end != ',')
return false;
p = end + 1;
errno = 0;
unsigned long rdev_minor = strtoul(p, &end, 10);
if (errno != 0 || end == p || rdev_minor > 0x00ffffffUL || *end != ' ')
return false;
p = end + 1;
errno = 0;
unsigned long uid = strtoul(p, &end, 10);
if (errno != 0 || end == p || uid > (unsigned long)UINT_MAX || *end != ':')
return false;
p = end + 1;
errno = 0;
unsigned long gid = strtoul(p, &end, 10);
if (errno != 0 || end == p || gid > (unsigned long)UINT_MAX)
return false;
p = end;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
if (*p != '\0')
return false;
*mode_out = (unsigned)mode;
*rdev_major_out = (unsigned)rdev_major;
*rdev_minor_out = (unsigned)rdev_minor;
*uid_out = (unsigned)uid;
*gid_out = (unsigned)gid;
return true;
}
/* --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
@@ -403,7 +454,7 @@ bool fake_super_restore_fd(int fd, FileAttrPolicy policy) {
return false; /* absent or filesystem without xattrs: silent no-op */
record[len] = '\0';
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)
if (!fake_super_parse_stat(record, &ul_mode, &rdev_major, &rdev_minor, &ul_uid, &ul_gid))
return false; /* malformed record: skip, never fatal */
/* --fake-super NEVER performs a real chown: that would defeat the whole point