#define _GNU_SOURCE #include "xattr.h" #include "identity.h" #include "log.h" #include "metadata.h" #include "protocol.h" #include "utils.h" #include "file_types.h" #include #include #include #include #include #include #include #include #include #include /* ---- lifecycle ---- */ FileXattrList* xattr_list_new(void) { FileXattrList* list = protocol_alloc(sizeof(FileXattrList)); if (!list) return NULL; list->items = NULL; list->count = 0; return list; } void xattr_list_free(FileXattrList* list) { if (!list) return; for (int i = 0; i < list->count; i++) { free(list->items[i].name); free(list->items[i].value); } free(list->items); free(list); } FileXattrList* xattr_list_clone(const FileXattrList* list) { if (!list) return NULL; FileXattrList* clone = xattr_list_new(); if (!clone) return NULL; for (int i = 0; i < list->count; i++) { if (!xattr_list_append(clone, list->items[i].name, list->items[i].value, list->items[i].value_len)) { xattr_list_free(clone); return NULL; } } return clone; } bool xattr_list_append(FileXattrList* list, const char* name, const void* value, size_t value_len) { if (!list || !name || (!value && value_len != 0)) return false; if (list->count >= XATTR_MAX_COUNT) return false; FileXattr* grown = realloc(list->items, ((size_t)list->count + 1) * sizeof(FileXattr)); if (!grown) return false; list->items = grown; size_t name_len = strlen(name); char* name_copy = malloc(name_len + 1); if (!name_copy) { return false; } unsigned char* value_copy = NULL; if (value_len > 0) { value_copy = malloc(value_len); if (!value_copy) { free(name_copy); return false; } memcpy(value_copy, value, value_len); } memcpy(name_copy, name, name_len); name_copy[name_len] = '\0'; list->items[list->count].name = name_copy; list->items[list->count].value = value_copy; list->items[list->count].value_len = value_len; list->count++; return true; } /* ---- namespace / length validation ---- */ /* A Linux xattr name is "namespace.name" with an optional leading "trusted.", * "system.", "security.", "user.", or "trusted." prefix. We only ever touch * the unprivileged "user.*" namespace and, only when --acls/-A was negotiated, * the two POSIX ACL xattrs carried in the "system." namespace. Everything else * -- especially "security.*" (ACLs, capabilities, SELinux labels) and * "trusted.*" -- is refused so a client can never compel the receiver to apply a * privileged attribute it would not otherwise be able to set (and which would be * a local privilege escalation if it could). * * The ACL gate is deliberate: --xattrs/-X alone derives use_xattrs but must NOT * authorize the ACL names, otherwise a -X client could plant an ACL the * receiver never opted into (B4). */ bool xattr_name_appliable(const char* name, bool preserve_acls) { if (!name || name[0] == '\0') return false; size_t len = strlen(name); if (len > XATTR_NAME_MAX) return false; /* The reserved --fake-super key is exclusively the RECEIVER's: it records the * source stat for a later privileged restore. A plain -X run must never * forward a source file that already carries this key (spoofable) onto the * destination, so it is excluded from capture AND from application. Only * fake_super_store_fd() writes it. */ if (strcmp(name, FAKESUPER_XATTR) == 0) return false; if (strncmp(name, "user.", 5) == 0) return name[5] != '\0'; if (strcmp(name, "system.posix_acl_access") == 0) return preserve_acls; if (strcmp(name, "system.posix_acl_default") == 0) return preserve_acls; return false; } /* The two POSIX ACL xattr names: the only names whose applicablity is * conditional (they require --acls). Used by the receiver to distinguish "not * negotiated" (drop the entry, keep user.* working for -X) from a genuinely * disallowed namespace (hard reject). */ static bool xattr_name_is_posix_acl(const char* name) { return name != NULL && (strcmp(name, "system.posix_acl_access") == 0 || strcmp(name, "system.posix_acl_default") == 0); } /* ---- SENDER: capture ---- */ FileXattrList* xattr_capture_path(const char* path, bool preserve_acls) { if (!path) return NULL; ssize_t list_size = listxattr(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); if (got < 0) { free(names); return NULL; } FileXattrList* list = xattr_list_new(); if (!list) { free(names); return NULL; } size_t budget = 0; ssize_t offset = 0; while (offset < got) { const char* name = names + offset; size_t name_len = strlen(name); if (name_len == 0) break; /* trailing double NUL not expected; stop */ offset += (ssize_t)name_len + 1; /* Capture is sender-side: the scanner has already gated on -X/-A, so the per-name whitelist here allows the ACL names only when --acls was 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); if (value_size < 0) continue; if (value_size > XATTR_VALUE_MAX) continue; /* oversize value is refused up front (bounded capture) */ if (name_len + (size_t)value_size > XATTR_TOTAL_MAX - budget) continue; /* would exceed the per-file budget: skip, keep the rest */ unsigned char* buffer = malloc(value_size > 0 ? (size_t)value_size : 1); if (!buffer) { xattr_list_free(list); free(names); return NULL; } ssize_t read_len = getxattr(path, name, buffer, (size_t)value_size); if (read_len < 0 || read_len != value_size) { free(buffer); continue; } if (!xattr_list_append(list, name, buffer, (size_t)value_size)) { free(buffer); xattr_list_free(list); free(names); return NULL; } free(buffer); budget += name_len + (size_t)value_size; } free(names); if (list->count == 0) { xattr_list_free(list); return NULL; } return list; } /* ---- WIRE ---- */ bool xattr_send(int fd, const FileXattrList* list) { int count = list ? list->count : 0; if (!send_int(fd, count)) return false; for (int i = 0; i < count; i++) { const FileXattr* xa = &list->items[i]; size_t name_len = strlen(xa->name); if (name_len > INT32_MAX) return false; int32_t name_len32 = (int32_t)name_len; if (xa->value_len > INT32_MAX) return false; int32_t value_len32 = (int32_t)xa->value_len; if (!send_n_data(fd, &name_len32, sizeof(name_len32)) || !send_n_data(fd, xa->name, name_len) || !send_n_data(fd, &value_len32, sizeof(value_len32)) || (value_len32 > 0 && !send_n_data(fd, xa->value, (size_t)value_len32))) return false; } return true; } FileXattrList* xattr_receive(int fd, int* ok, bool preserve_acls) { if (ok) *ok = 0; int count; if (!receive_int(fd, &count)) return NULL; if (count < 0 || count > XATTR_MAX_COUNT) { log_message(LOG_LEVEL_ERROR, "rejected xattr block: invalid attribute count %d", count); return NULL; } FileXattrList* list = xattr_list_new(); if (!list) return NULL; size_t budget = 0; for (int i = 0; i < count; i++) { int32_t name_len32; if (!receive_n_data(fd, &name_len32, sizeof(name_len32))) { xattr_list_free(list); return NULL; } if (name_len32 <= 0 || name_len32 > XATTR_NAME_MAX) { log_message(LOG_LEVEL_ERROR, "rejected xattr block: invalid name length %d", name_len32); xattr_list_free(list); return NULL; } char* name = protocol_alloc((size_t)name_len32 + 1); if (!name) { xattr_list_free(list); return NULL; } if (!receive_n_data(fd, name, (size_t)name_len32)) { free(name); xattr_list_free(list); return NULL; } name[name_len32] = '\0'; if (memchr(name, '\0', (size_t)name_len32) != NULL) { /* embedded NUL in the name: malformed, reject */ free(name); xattr_list_free(list); return NULL; } bool skip = false; if (!xattr_name_appliable(name, preserve_acls)) { if (!preserve_acls && xattr_name_is_posix_acl(name)) { /* -X without -A: the sender may still carry ACLs, but the receiver must never apply an ACL it was not asked to preserve. Consume and drop the entry (keeping -X compatibility) rather than failing the transfer. */ skip = true; } else { log_message(LOG_LEVEL_ERROR, "rejected xattr block: disallowed namespace for '%s'", name); free(name); xattr_list_free(list); return NULL; } } int32_t value_len32; if (!receive_n_data(fd, &value_len32, sizeof(value_len32))) { free(name); xattr_list_free(list); return NULL; } if (value_len32 < 0 || value_len32 > XATTR_VALUE_MAX) { log_message(LOG_LEVEL_ERROR, "rejected xattr block: invalid value length %d for '%s'", value_len32, name); free(name); xattr_list_free(list); return NULL; } if ((size_t)name_len32 + (size_t)value_len32 > XATTR_TOTAL_MAX - budget) { log_message(LOG_LEVEL_ERROR, "rejected xattr block: total size budget exceeded for '%s'", name); free(name); xattr_list_free(list); return NULL; } unsigned char* value = NULL; if (value_len32 > 0) { value = protocol_alloc((size_t)value_len32); if (!value) { free(name); xattr_list_free(list); return NULL; } if (!receive_n_data(fd, value, (size_t)value_len32)) { free(value); free(name); xattr_list_free(list); return NULL; } } if (skip) { free(value); free(name); budget += (size_t)name_len32 + (size_t)value_len32; continue; } if (!xattr_list_append(list, name, value, (size_t)value_len32)) { free(value); free(name); xattr_list_free(list); return NULL; } free(value); free(name); budget += (size_t)name_len32 + (size_t)value_len32; } if (ok) *ok = 1; return list; } /* ---- RECEIVER: apply (fd-relative, best-effort) ---- */ bool xattr_apply_fd(int fd, const FileXattrList* list) { if (fd < 0 || !list) return false; 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 (fsetxattr(fd, xa->name, xa->value, xa->value_len, 0) != 0) { if (!warned) { warned = true; first_errno = errno; } } } /* Collapse potentially many per-attribute failures into one per-file warning so a run with many unsettable attributes does not spam the log. */ if (warned) log_message(LOG_LEVEL_WARNING, "could not set one or more xattrs on the destination file: %s", strerror(first_errno)); 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, uint32_t rdev_minor) { if (fd < 0) return; /* rsync 3.4.1's exact grammar: " , * :". 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) { log_message(LOG_LEVEL_WARNING, "--fake-super: could not store %s on destination file: %s", FAKESUPER_XATTR, strerror(errno)); } } /* Parse rsync's `user.rsync.%stat` grammar strictly: * " , :" * 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 * 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; char record[128]; ssize_t len = fgetxattr(fd, FAKESUPER_XATTR, record, sizeof(record) - 1); if (len < 0) 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 (!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 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). 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; mode_t want = 0; 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 & 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)); } } return true; }