feat(xattr): carry and apply symlink xattrs (protocol 2.29.0)
This commit is contained in:
+15
-2
@@ -83,7 +83,7 @@ typedef struct {
|
||||
typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF = 2 } SuperMode;
|
||||
|
||||
/* ===========================================================================
|
||||
* Config wire-field table (single source of truth for protocol 2.28.0).
|
||||
* Config wire-field table (single source of truth for protocol 2.29.0).
|
||||
*
|
||||
* Every field below crosses the wire. The table is the ONLY place a
|
||||
* serialized field is named: config.h expands CONFIG_WIRE_FIELDS() to declare
|
||||
@@ -1071,7 +1071,20 @@ typedef struct Config {
|
||||
* filter rules so the receiver can protect DESTINATION-ONLY entries from
|
||||
* --delete with `protect`/`risk` rules (rsync parity). The block appends after
|
||||
* compression_algo; see CONFIG_WIRE_PROTECT_FIELDS. */
|
||||
#define PROTOCOL_VERSION "2.28.0"
|
||||
/* (10) Symlink xattrs/ACLs (protocol 2.29.0): the config-frame LAYOUT is
|
||||
* unchanged (the derived use_xattrs bit already crosses the wire), but the
|
||||
* STATUS_SYMLINK frame BODY grows a trailing bounded xattr block when -X/-A is
|
||||
* negotiated -- exactly the block STATUS_MKDIR, STATUS_DIR_TIMES and regular
|
||||
* files already carry. The sender captures the symlink's OWN xattrs with
|
||||
* llistxattr/lgetxattr (so it can never attach the REFERENT's attributes to the
|
||||
* link) and the receiver re-applies them to the link itself with lsetxattr on a
|
||||
* confined /proc/self/fd/<parent>/<leaf> path (there is no *at xattr syscall and
|
||||
* fsetxattr cannot target a symlink). A 2.28 peer that does not consume the new
|
||||
* trailing block would desynchronize after every symlink, so the protocol
|
||||
* version must bump; the strict same-version handshake (config_receive rejects a
|
||||
* mismatched version before parsing anything else) keeps a 2.29 client and a
|
||||
* 2.28 server from ever reaching that state. */
|
||||
#define PROTOCOL_VERSION "2.29.0"
|
||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
||||
#define MAX_BASIS_DIRS 64
|
||||
|
||||
@@ -482,6 +482,14 @@ File* file_receive_symlink(int file_descriptor, const Config* config) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
/* Symlink xattrs/ACLs (-X/-A) arrive in the same trailing block as the other
|
||||
entry kinds; the block is present iff use_xattrs (which itself implies
|
||||
use_metadata, so the metadata frame above is always consumed first). */
|
||||
if (config && !receive_file_xattrs(file, file_descriptor, config)) {
|
||||
file_destroy(file);
|
||||
free(target);
|
||||
return NULL;
|
||||
}
|
||||
file->is_symlink = true;
|
||||
file->symlink_target = target;
|
||||
return file;
|
||||
|
||||
@@ -923,6 +923,19 @@ static FileSaveResult file_save_symlink_to_disk(const FileSavePlan* plan, bool*
|
||||
ok = file_restore_symlink_metadata(link_path, file->metadata, link_policy,
|
||||
config->omit_link_times);
|
||||
}
|
||||
/* -X/-A: apply the symlink's OWN xattrs with a no-follow primitive. The
|
||||
confined parent directory is the anchor and the final component is applied
|
||||
with lsetxattr, so the referent is never touched. Best-effort: on Linux
|
||||
the VFS refuses xattrs on symlinks, so this is normally a no-op. */
|
||||
if (ok && config && config->use_xattrs && file->xattrs) {
|
||||
char* leaf = NULL;
|
||||
int parent_fd = file_open_secure_parent(link_path, &leaf, false);
|
||||
if (parent_fd >= 0) {
|
||||
xattr_apply_path_nofollow(parent_fd, leaf, file->xattrs);
|
||||
close(parent_fd);
|
||||
}
|
||||
free(leaf);
|
||||
}
|
||||
if (ok && created && !link_existed)
|
||||
*created = true;
|
||||
free(link_path);
|
||||
|
||||
+63
-5
@@ -134,16 +134,23 @@ static bool xattr_name_is_posix_acl(const char* name) {
|
||||
|
||||
/* ---- SENDER: capture ---- */
|
||||
|
||||
FileXattrList* xattr_capture_path(const char* path, bool preserve_acls) {
|
||||
/* The two syscall families differ only in whether the FINAL component is
|
||||
* followed (`listxattr`/`getxattr` follow; `llistxattr`/`lgetxattr` do not), so
|
||||
* one common implementation backs both public entry points. */
|
||||
typedef ssize_t (*XattrListFn)(const char* path, char* list, size_t size);
|
||||
typedef ssize_t (*XattrGetFn)(const char* path, const char* name, void* value, size_t size);
|
||||
|
||||
static FileXattrList* xattr_capture_common(const char* path, bool preserve_acls,
|
||||
XattrListFn list_fn, XattrGetFn get_fn) {
|
||||
if (!path)
|
||||
return NULL;
|
||||
ssize_t list_size = listxattr(path, NULL, 0);
|
||||
ssize_t list_size = list_fn(path, NULL, 0);
|
||||
if (list_size <= 0)
|
||||
return NULL; /* no xattrs, ENOTSUP, or error: nothing appliable */
|
||||
char* names = malloc((size_t)list_size);
|
||||
if (!names)
|
||||
return NULL;
|
||||
ssize_t got = listxattr(path, names, (size_t)list_size);
|
||||
ssize_t got = list_fn(path, names, (size_t)list_size);
|
||||
if (got < 0) {
|
||||
free(names);
|
||||
return NULL;
|
||||
@@ -166,7 +173,7 @@ FileXattrList* xattr_capture_path(const char* path, bool preserve_acls) {
|
||||
negotiated. Without it a plain -X capture never carries an ACL. */
|
||||
if (!xattr_name_appliable(name, preserve_acls))
|
||||
continue;
|
||||
ssize_t value_size = getxattr(path, name, NULL, 0);
|
||||
ssize_t value_size = get_fn(path, name, NULL, 0);
|
||||
if (value_size < 0)
|
||||
continue;
|
||||
if (value_size > XATTR_VALUE_MAX)
|
||||
@@ -179,7 +186,7 @@ FileXattrList* xattr_capture_path(const char* path, bool preserve_acls) {
|
||||
free(names);
|
||||
return NULL;
|
||||
}
|
||||
ssize_t read_len = getxattr(path, name, buffer, (size_t)value_size);
|
||||
ssize_t read_len = get_fn(path, name, buffer, (size_t)value_size);
|
||||
if (read_len < 0 || read_len != value_size) {
|
||||
free(buffer);
|
||||
continue;
|
||||
@@ -201,6 +208,14 @@ FileXattrList* xattr_capture_path(const char* path, bool preserve_acls) {
|
||||
return list;
|
||||
}
|
||||
|
||||
FileXattrList* xattr_capture_path(const char* path, bool preserve_acls) {
|
||||
return xattr_capture_common(path, preserve_acls, listxattr, getxattr);
|
||||
}
|
||||
|
||||
FileXattrList* xattr_capture_path_nofollow(const char* path, bool preserve_acls) {
|
||||
return xattr_capture_common(path, preserve_acls, llistxattr, lgetxattr);
|
||||
}
|
||||
|
||||
/* ---- WIRE ---- */
|
||||
|
||||
bool xattr_send(int fd, const FileXattrList* list) {
|
||||
@@ -364,6 +379,49 @@ bool xattr_apply_fd(int fd, const FileXattrList* list) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Symlink counterpart of xattr_apply_fd(): target the link ITSELF, never its
|
||||
* referent. fsetxattr cannot be used (no *at xattr syscall exists, and the
|
||||
* kernel rejects xattr syscalls on an O_PATH descriptor), so the already-open,
|
||||
* confinement-checked parent directory is addressed through /proc/self/fd and
|
||||
* the final component is applied with lsetxattr, which does not follow it. */
|
||||
bool xattr_apply_path_nofollow(int parent_fd, const char* leaf, const FileXattrList* list) {
|
||||
if (parent_fd < 0 || !leaf || leaf[0] == '\0' || strchr(leaf, '/') != NULL || !list)
|
||||
return false;
|
||||
if (list->count == 0)
|
||||
return true;
|
||||
char prefix[64];
|
||||
int prefix_len = snprintf(prefix, sizeof(prefix), "/proc/self/fd/%d/", parent_fd);
|
||||
if (prefix_len < 0 || (size_t)prefix_len >= sizeof(prefix))
|
||||
return false;
|
||||
size_t leaf_len = strlen(leaf);
|
||||
char* path = malloc((size_t)prefix_len + leaf_len + 1);
|
||||
if (!path)
|
||||
return false;
|
||||
memcpy(path, prefix, (size_t)prefix_len);
|
||||
memcpy(path + prefix_len, leaf, leaf_len + 1);
|
||||
bool warned = false;
|
||||
int first_errno = 0;
|
||||
for (int i = 0; i < list->count; i++) {
|
||||
const FileXattr* xa = &list->items[i];
|
||||
/* Defense in depth: even a hand-crafted list can never apply the reserved
|
||||
--fake-super key (only fake_super_store_fd may write it). */
|
||||
if (strcmp(xa->name, FAKESUPER_XATTR) == 0)
|
||||
continue;
|
||||
if (lsetxattr(path, xa->name, xa->value, xa->value_len, 0) != 0) {
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
first_errno = errno;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (warned)
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"could not set one or more xattrs on the destination symlink: %s",
|
||||
strerror(first_errno));
|
||||
free(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---- --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, uint32_t rdev_major,
|
||||
|
||||
+29
-2
@@ -26,8 +26,11 @@
|
||||
* and total bytes) on BOTH ends to prevent OOM/memory abuse; an oversized
|
||||
* or malformed frame is a clean protocol rejection, never an allocation
|
||||
* blowup.
|
||||
* * Application is confined to the exact destination file descriptor
|
||||
* (fsetxattr on the just-written fd), never a caller-controlled path.
|
||||
* * Application is confined to the exact destination entry: fsetxattr on the
|
||||
* just-written fd for regular files/directories, and for a symlink an
|
||||
* lsetxattr on "/proc/self/fd/<parent_fd>/<leaf>" reached through the
|
||||
* already-opened, confinement-checked parent directory -- never a
|
||||
* caller-controlled path, and never following the link.
|
||||
*/
|
||||
|
||||
/* Reserved key used by --fake-super to park the source's privileged ownership
|
||||
@@ -79,6 +82,17 @@ bool xattr_name_appliable(const char* name, bool preserve_acls);
|
||||
* distinct from NULL. */
|
||||
FileXattrList* xattr_capture_path(const char* path, bool preserve_acls);
|
||||
|
||||
/* Sender: like xattr_capture_path() but reads the xattrs of `path` ITSELF,
|
||||
* never following a final symlink (llistxattr/lgetxattr). A symlink entry must
|
||||
* use this so the scanner never captures the REFERENT's attributes onto the
|
||||
* link (the path-following variant would). On Linux the VFS refuses to
|
||||
* associate xattrs with symlinks at all, so this normally returns NULL; it is
|
||||
* still correct and portable for a filesystem/platform that supports them.
|
||||
* The same whitelist/bounds as xattr_capture_path() apply. Returns NULL when
|
||||
* the link has no appliable xattrs (or the filesystem does not support them);
|
||||
* an empty-but-valid list is never returned distinct from NULL. */
|
||||
FileXattrList* xattr_capture_path_nofollow(const char* path, bool preserve_acls);
|
||||
|
||||
/* Wire: bounded serialization. xattr_send returns false on write failure; an
|
||||
* empty/NULL list transmits a zero-count block. xattr_receive returns NULL and
|
||||
* sets *ok = 0 on any malformed / oversized / non-whitelisted entry. When
|
||||
@@ -94,6 +108,19 @@ 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);
|
||||
|
||||
/* Receiver: apply every entry to the symlink named by (parent_fd, leaf) WITHOUT
|
||||
* following it, via lsetxattr() on the confined path
|
||||
* "/proc/self/fd/<parent_fd>/<leaf>". A symlink cannot be targeted by the
|
||||
* fd-relative fsetxattr() path: there is no *at() xattr syscall and the kernel
|
||||
* rejects xattr syscalls on an O_PATH descriptor, so the already-opened,
|
||||
* confinement-checked parent directory is the anchor and only the final
|
||||
* component is the (no-follow) link. `leaf` must be a single path component.
|
||||
* Best-effort exactly like xattr_apply_fd(): a per-attribute failure (on Linux
|
||||
* every set on a symlink fails with EPERM) is logged once and skipped, never
|
||||
* fatal. Returns false only for an invalid anchor/list; true when an apply was
|
||||
* attempted. The reserved --fake-super key is never applied. */
|
||||
bool xattr_apply_path_nofollow(int parent_fd, const char* leaf, const FileXattrList* list);
|
||||
|
||||
/* --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.
|
||||
|
||||
Reference in New Issue
Block a user