feat(xattr): rsync-interoperable --fake-super stat; fix --devices error parity
This commit is contained in:
+6
-4
@@ -735,11 +735,13 @@ typedef struct Config {
|
||||
* --copy-as) imply it. */
|
||||
/* fake_super */
|
||||
/* --fake-super: receiver-only. When set, each written file additionally gets
|
||||
* a reserved user.fastsync.stat xattr recording the RESOLVED uid/gid (the
|
||||
* rsync's reserved user.rsync.%stat xattr recording the RESOLVED uid/gid (the
|
||||
* source's own when no ownership request is active, else the --chown/--usermap
|
||||
* result) plus mode/mtime so a later privileged restore could re-apply them.
|
||||
* It NEVER real-chowns: the point is to record the source ownership on an
|
||||
* unprivileged receiver. Crosses the wire. */
|
||||
* result) plus the full mode and rdev, in rsync 3.4.1's grammar, so the tree is
|
||||
* interoperable and a later privileged restore could re-apply them. mtime is
|
||||
* carried by the file's own timestamp, exactly as rsync does it. It NEVER
|
||||
* real-chowns: the point is to record the source ownership on an unprivileged
|
||||
* receiver. Crosses the wire. */
|
||||
/* module */
|
||||
/* Daemon module selection (Wave A, protocol 2.15.0). Client-composed from a
|
||||
* host::module/path destination; NULL or "" means "no module" (the ordinary
|
||||
|
||||
+5
-4
@@ -1189,14 +1189,15 @@ static void restore_extra_fd(int fd, const FileMetadata* metadata, const FileXat
|
||||
ownership request (--chown/--usermap/--groupmap/--copy-as or -o/-g) is
|
||||
active, the resolved mapping; otherwise the source's own id. The real
|
||||
chown is suppressed (identity_apply_ownership early-returns under
|
||||
--fake-super) so recording never defeats the flag. Mode/mtime are still
|
||||
replayed (policy-gated) so unprivileged --fake-super keeps working. */
|
||||
--fake-super) so recording never defeats the flag. The recorded stat is
|
||||
rsync's format; the permission bits are replayed (policy-gated) so
|
||||
unprivileged --fake-super keeps working while mtime comes from the
|
||||
normal metadata path above. */
|
||||
uint32_t store_uid;
|
||||
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, metadata->mtime_sec,
|
||||
metadata->mtime_nsec);
|
||||
fake_super_store_fd(fd, store_uid, store_gid, (uint32_t)metadata->mode, 0, 0);
|
||||
fake_super_restore_fd(fd, policy);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-9
@@ -353,11 +353,13 @@ bool file_special_rdev_valid(int32_t major, int32_t minor, mode_t mode) {
|
||||
/* ---- Device/special node RECREATION (--devices/--specials), receiver side ----
|
||||
*
|
||||
* Privilege gating: making a real device node requires CAP_MKNOD (root); making
|
||||
* a FIFO works unprivileged (mkfifo). When the receiver lacks the capability,
|
||||
* mknodat() fails with EPERM and the entry is SKIPPED with a warning -- the
|
||||
* whole transfer must NOT abort just because the environment cannot make the
|
||||
* node. CI runs non-root, so device creation is expected to skip there and
|
||||
* only a FIFO is honestly assertable unprivileged.
|
||||
* 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.
|
||||
*
|
||||
* Confinement: the parent directory is opened fd-relative below the receive
|
||||
* root (file_open_secure_parent: O_NOFOLLOW, no "..", root-checked) and the
|
||||
@@ -495,13 +497,32 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
node_kind, escaped_path ? escaped_path : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
} else if (errno == EPERM || errno == EACCES) {
|
||||
/* Missing CAP_MKNOD / parent write permission: the environment cannot
|
||||
create the node, so skip instead of failing the whole run. */
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
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
|
||||
* (--specials) keeps the best-effort skip path below. */
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"cannot create %s %s: %s\n"
|
||||
" --devices node creation needs privilege (CAP_MKNOD)",
|
||||
node_kind, shown_path, strerror(errno));
|
||||
free(escaped_path);
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
free(destination);
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
/* Missing CAP_MKNOD / parent write permission for a FIFO/socket: the
|
||||
environment cannot create the node, so skip instead of failing the
|
||||
whole run. */
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"skipping %s: cannot create %s node (%s)\n"
|
||||
" --devices/--specials node creation needs privilege (CAP_MKNOD)",
|
||||
escaped_path ? escaped_path : "<allocation failed>", node_kind, strerror(errno));
|
||||
" --specials node creation needs privilege (CAP_MKNOD)",
|
||||
shown_path, node_kind, strerror(errno));
|
||||
free(escaped_path);
|
||||
} else {
|
||||
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
|
||||
|
||||
+35
-33
@@ -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;
|
||||
}
|
||||
|
||||
+28
-21
@@ -31,11 +31,14 @@
|
||||
*/
|
||||
|
||||
/* Reserved key used by --fake-super to park the source's privileged ownership
|
||||
* / mode / mtime on the destination file as an unprivileged user.* xattr, so a
|
||||
* later privileged restore could re-apply them. Exact documented format:
|
||||
* uid:gid:mode:mtime_sec:mtime_nsec (decimal, decimal, octal, dec, dec)
|
||||
* e.g. "1000:1000:644:1765238400:0". */
|
||||
#define FAKESUPER_XATTR "user.fastsync.stat"
|
||||
* / mode / rdev on the destination file as an unprivileged user.* xattr, so the
|
||||
* tree is interoperable with rsync 3.4.1 and a later privileged restore can
|
||||
* re-apply them. This is rsync's own key and value grammar exactly:
|
||||
* <octal st_mode with S_IFMT> <rdev_major>,<rdev_minor> <uid>:<gid>
|
||||
* e.g. "104711 0,0 1234:5678" for a setuid regular file owned by 1234:5678,
|
||||
* or "20644 1,3 111:222" for a char device. mtime is deliberately NOT part of
|
||||
* the record: exactly like rsync, the file's own timestamp carries it. */
|
||||
#define FAKESUPER_XATTR "user.rsync.%stat"
|
||||
|
||||
/* --- bounds --- */
|
||||
#define XATTR_NAME_MAX 255 /* xattr names are limited to 255 bytes */
|
||||
@@ -91,24 +94,28 @@ FileXattrList* xattr_receive(int fd, int* ok, bool preserve_acls);
|
||||
* true when apply was attempted (allowing callers to treat it as best-effort). */
|
||||
bool xattr_apply_fd(int fd, const FileXattrList* list);
|
||||
|
||||
/* --fake-super: write the source uid/gid/mode/mtime record into the reserved
|
||||
* FAKESUPER_XATTR on `fd`. 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, int64_t mtime_sec,
|
||||
int64_t mtime_nsec);
|
||||
/* --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. */
|
||||
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 mode/mtime 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. 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 MTIME leg only when policy.times, so the fake-super
|
||||
* replay cannot bypass the per-attribute split; the mode follows the normal
|
||||
* metadata path exactly (under --perms the source mode is copied verbatim,
|
||||
* special and group/other write bits included).
|
||||
* Returns true when the xattr was present and parsed. */
|
||||
* `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
|
||||
* 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
|
||||
* 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
|
||||
* timestamp. Returns true when the xattr was present and parsed. */
|
||||
bool fake_super_restore_fd(int fd, FileAttrPolicy policy);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user