fix(xattr): fake-super device round-trip, --devices continue-on-error, harden stat parse
This commit is contained in:
+27
-21
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user