Files
FastSync/src/shared/xattr.h
T

168 lines
9.6 KiB
C

#ifndef XATTR_H
#define XATTR_H
#include "file_attr.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/*
* Portable extended-attribute (xattr) and POSIX-ACL preservation (Phase 4,
* protocol 2.13.0). --xattrs/-X and --acls/-A are implemented on top of the
* xattr machinery: the SENDER captures a bounded, namespace-whitelisted set of
* `name = value` pairs per file, transmits them in a per-file wire block, and
* the RECEIVER re-applies them fd-relative on the just-written file. Linux
* xattr syscalls are used; libacl is NOT required (ACLs travel as the
* system.posix_acl_access / system.posix_acl_default xattrs).
*
* Security model:
* * A client can never force a `security.*` / privileged xattr onto the
* destination: both capture (sender) and apply (receiver) are restricted to
* the unprivileged `user.*` namespace and the two POSIX ACL xattrs. The
* receiver independently re-validates every incoming name against this
* whitelist, so a malicious sender's `security.capability` payload is
* rejected, not applied.
* * Payloads are bounded (per-name length, per-value length, per-file count
* 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 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
* / 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 */
#define XATTR_VALUE_MAX (1024 * 1024) /* per-value cap (1 MiB) */
#define XATTR_TOTAL_MAX (4 * 1024 * 1024) /* per-file total name+value bytes */
#define XATTR_MAX_COUNT 256
typedef struct {
char* name; /* owned, NUL-terminated */
unsigned char* value; /* owned, may hold embedded NULs */
size_t value_len;
} FileXattr;
typedef struct {
FileXattr* items;
int count;
} FileXattrList;
FileXattrList* xattr_list_new(void);
void xattr_list_free(FileXattrList* list);
/* Deep-copy `list` (NULL in, NULL out). Returns NULL on allocation failure. */
FileXattrList* xattr_list_clone(const FileXattrList* list);
/* Append one entry (deep copy). Returns false on allocation failure. */
bool xattr_list_append(FileXattrList* list, const char* name, const void* value, size_t value_len);
/* True when `name` is a well-formed xattr name AND belongs to a namespace this
* build is authorized to apply. `user.*` is always accepted for -X; the two
* POSIX ACL xattrs are accepted only when `preserve_acls` (--acls/-A) is set, so
* a plain -X run can never carry or apply an ACL the receiver did not ask for.
* Used for both capture and receiver-side validation. */
bool xattr_name_appliable(const char* name, bool preserve_acls);
/* Sender: read the whitelisted xattrs of `path` into a new list. The POSIX ACL
* names are captured only when `preserve_acls` (--acls/-A) is set, so a plain
* -X run never carries an ACL it was not asked to preserve; `user.*` is
* unaffected. Returns NULL when the path has no appliable xattrs (or the
* filesystem has no xattr support); an empty-but-valid list is never returned
* 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
* `preserve_acls` is false, any POSIX ACL entries are consumed and DROPPED (so
* a -X transfer still succeeds and never applies an ACL it did not negotiate);
* a genuinely disallowed namespace is still rejected. */
bool xattr_send(int fd, const FileXattrList* list);
FileXattrList* xattr_receive(int fd, int* ok, bool preserve_acls);
/* Receiver: apply every entry fd-relative (fsetxattr) to the just-written file
* descriptor. A per-attribute failure (e.g. ACL set refused for non-root on a
* file the process does not own) is logged and skipped, never fatal. Returns
* 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>". Every incoming name is independently
* re-validated against xattr_name_appliable() with `preserve_acls`, exactly like
* xattr_apply_fd(): a non-whitelisted namespace (including the reserved
* --fake-super key) is skipped, so this primitive stays confined even if handed
* a hand-crafted list. 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.
*
* Portability: the "/proc/self/fd/<parent_fd>" anchor requires a mounted /proc.
* Where /proc is unavailable (or the fd cannot be addressed that way) the
* lsetxattr simply fails and is skipped -- the apply is best-effort exactly like
* xattr_apply_fd(), so no error is propagated and the transfer continues. 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.
*
* Residual TOCTOU: `leaf` is a caller-supplied name resolved by path in the
* parent, so a local writer could replace the just-created symlink between its
* creation and lsetxattr(). This is bounded: it requires write access to the
* confinement-checked destination directory (already trusted), can only install
* a whitelisted user namespace or POSIX-ACL name, and never follows the link (a
* replacement symlink is still applied to as the final, no-follow component). */
bool xattr_apply_path_nofollow(int parent_fd, const char* leaf, const FileXattrList* list,
bool preserve_acls);
/* --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.
* `fd` may be a regular file, a faked char/block device (written as a regular
* file), or a DIRECTORY: rsync stores a directory's faked mode/uid/gid in the
* reserved xattr on the directory itself. 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 the recorded permission bits
* fd-relative. `fd` may be a regular file, a faked device, or a DIRECTORY;
* fgetxattr/fchmod work identically on a directory descriptor. 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 entry -- 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