acls/xattrs: -X/--xattrs, -A/--acls, --fake-super
CI / lint (pull_request) Failing after 55s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
CI / lint (pull_request) Failing after 55s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
New src/shared/xattr.{c,h}: capture user.* + POSIX ACL xattrs, transmit a bounded
per-file block, re-apply fd-relative. security.*/trusted.*/other system.* never
transmitted/applied (receiver re-validates). Bounds: name<=255 value<=1MiB count
<=256 total<=4MiB. --fake-super records uid:gid:mode:mtime in reserved
user.fastsync.stat (receiver-only). PROTOCOL_VERSION 2.12.0->2.13.0. Review
fixes: reserved key not forwardable, link/hardlink copy-fallback preserves
xattrs, no const-param mutation, per-file warning dedup.
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
#define _GNU_SOURCE
|
||||
#include "xattr.h"
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include "utils.h"
|
||||
#include "file_types.h"
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/xattr.h>
|
||||
|
||||
/* ---- 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);
|
||||
}
|
||||
|
||||
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 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). */
|
||||
bool xattr_name_appliable(const char* name) {
|
||||
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 true;
|
||||
if (strcmp(name, "system.posix_acl_default") == 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ---- SENDER: capture ---- */
|
||||
|
||||
FileXattrList* xattr_capture_path(const char* path) {
|
||||
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;
|
||||
if (!xattr_name_appliable(name))
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
if (!xattr_name_appliable(name)) {
|
||||
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 (!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/mtime 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) {
|
||||
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);
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user