Three receiver security fixes from the audit: 1. --temp-dir symlink escape (High): file_open_temp_dir() opened the client-controlled scratch dir with a bare open(), so a symlink planted under the receive root let a peer redirect receiver scratch files outside the authorized root. The opened dir is now judged by the REAL path of its fd (via /proc/self/fd), and any target outside the authorized receive root is refused with a logged error (EACCES). An in-root symlink (the EXDEV cross-filesystem fallback case) still works, and the no-root local batch path is unchanged. 2. setuid/setgid/sticky under SUPER_MODE_OFF (High): the special bits were applied under --perms (and via --chmod) even when the connection forbade super-user activities. FileAttrPolicy gains super_permitted, set by file_attr_policy_from_config() from privilege_super_mode_permitted(); metadata_mode_for_policy(), the symlink path, the special-node creation path, and the deferred directory-mode apply now strip the special bits when it is false. Exact rsync semantics are preserved when permitted. 3. daemon umask (Low): daemonize() forced umask(0), so implied parent directories created without -p were world-writable 0777. Set the conventional daemon umask 022 instead (rsync never forces 0); -p/-a mode preservation is unaffected because it restores modes via fchmod. Tests: new unit tests for file_open_temp_dir confinement and the masked/unmasked special-bit policy (incl. the --chmod path), a daemon world-writable-dir regression test, an integration escape test, and a root-only integration test asserting special bits are masked without --allow-super. The old cross-filesystem test encoded the vulnerable behavior (symlink target outside the root) and is replaced by the escape test; the EXDEV fallback code is retained for in-root links.
204 lines
13 KiB
C
204 lines
13 KiB
C
#ifndef FILE_H
|
|
#define FILE_H
|
|
|
|
#include "file_send.h"
|
|
#include "file_receive.h"
|
|
#include "file_types.h"
|
|
#include "checksum.h"
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <sys/stat.h>
|
|
|
|
/* File/FileMetadata lifecycle, local disk helpers, and secure filesystem
|
|
primitives shared by the send/receive pipelines. */
|
|
|
|
File* file_create(const char* path);
|
|
void file_destroy(void* item);
|
|
bool file_load_data(File* file);
|
|
/* Compute the whole-file content digest of `file` with the negotiated
|
|
* --checksum-choice algorithm and --checksum-seed. Writes the digest into
|
|
* `out` (capacity `out_capacity`) and its length into `*out_len`. Returns
|
|
* false on read/allocation failure or when the digest would not fit. */
|
|
bool file_checksum(File* file, ChecksumAlgo algo, uint64_t seed, uint8_t* out, size_t out_capacity,
|
|
size_t* out_len);
|
|
size_t file_content_to_buffer(File* file);
|
|
FileMetadata* file_metadata_create(const char* path, const struct stat* stats, bool capture_atime,
|
|
bool capture_crtime);
|
|
void file_metadata_destroy(void* metadata);
|
|
/* --open-noatime process-wide sender policy; see file.c. */
|
|
void file_set_open_noatime(bool enable);
|
|
bool file_get_open_noatime(void);
|
|
/* Capture the process umask ONCE, before any threads are created. Call this at
|
|
* the very top of main() in both entry points so the cached value is read while
|
|
* the process is still single-threaded: reading the umask needs a get+set round
|
|
* trip (umask(0); umask(old)), which would race against receiver threads
|
|
* creating files if it happened during the first write. Idempotent and safe to
|
|
* call more than once. */
|
|
void file_umask_capture(void);
|
|
/* Process-wide umask, captured once (thread-safe). Used to derive the mode of
|
|
* a brand-new destination like rsync: source_mode & 0777 & ~umask. Falls back
|
|
* to file_umask_capture() (behind pthread_once) if capture was never called. */
|
|
unsigned file_process_umask(void);
|
|
/* Open `path` read-only for transfer, honouring --open-noatime when set. */
|
|
int file_open_for_read(const char* path);
|
|
bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size,
|
|
bool inplace, bool sparse);
|
|
|
|
/* Symlink trust-boundary helpers (Phase 4, symlink wave; rsync parity).
|
|
* --munge-links is a RECEIVER-side rewrite: rsync prefixes every stored symlink
|
|
* target with this marker, making the link unusable while the referenced
|
|
* directory does not exist. A SENDER receiving a munged source strips it back
|
|
* off before transmitting (so a munged tree round-trips through the receiver's
|
|
* re-munging). */
|
|
#define SYMLINK_MUNGE_PREFIX "/rsyncd-munged/"
|
|
|
|
char* file_symlink_munge(const char* target);
|
|
/* rsync 3.4.1 unsafe_symlink(): true when `target` escapes the transfer tree
|
|
* rooted at `link_path` (the symlink's transfer-relative path incl. its name).
|
|
* Absolute/empty targets and targets climbing above the transfer root (via
|
|
* "..") are unsafe, as are internal "/../" components and trailing "/..". */
|
|
bool file_symlink_unsafe(const char* target, const char* link_path);
|
|
/* True when a lexical target is relative and contains no ".." component, so it
|
|
* can never escape the receive root once created beneath it. */
|
|
bool file_symlink_target_contained(const char* target);
|
|
/* Strip a leading SYMLINK_MUNGE_PREFIX from `target` (mutable, in place);
|
|
* returns true when a marker was removed. */
|
|
bool file_symlink_unmunge(char* target);
|
|
/* Create a symlink at `path` -> `target`, confined below the authorized root
|
|
* (O_NOFOLLOW parent walk, symlinkat; the target is never followed). The link
|
|
* value is copied verbatim (rsync -l); only the placement path is confined.
|
|
* Returns false when a directory already occupies `path`. */
|
|
bool file_symlink_at_secure(const char* path, const char* target);
|
|
/* --keep-dirlinks (-K) receiver process-wide policy: allow an in-root existing
|
|
* symlink-to-directory to be followed as a directory. */
|
|
void file_set_keep_dirlinks(bool enable);
|
|
|
|
/* --trust-sender receiver process-wide policy (Phase 5). When set, the
|
|
* receiver trusts that the sender already produced a clean file list and skips
|
|
* its own redundant up-front re-validation of incoming paths (the empty/".."
|
|
* rejection and the escaping-symlink-target containment). The low-level
|
|
* fd-relative confinement primitives below are deliberately NOT disabled by
|
|
* this flag, so a hostile sender still cannot escape the authorized root. */
|
|
void file_set_trust_sender(bool enable);
|
|
bool file_get_trust_sender(void);
|
|
|
|
/* Secure path/filesystem primitives (symlink-safe, O_NOFOLLOW, root-confined). */
|
|
bool file_path_exists_secure(const char* path);
|
|
bool file_stat_secure(const char* path, struct stat* st);
|
|
bool file_destination_is_newer_secure(const char* path, const FileMetadata* metadata);
|
|
int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs);
|
|
/* Protocol 2.28.0 variant: also increments *dirs_created for every missing
|
|
* parent directory this walk creates that lies strictly below `count_floor`
|
|
* (a receive-root-relative path, or NULL to count all of them). */
|
|
int file_open_secure_parent_counted(const char* path, char** leaf_out, bool create_dirs,
|
|
unsigned* dirs_created, const char* count_floor);
|
|
bool file_ensure_directory_secure(const char* path);
|
|
bool file_directory_exists_secure(const char* path);
|
|
bool file_rename_secure(const char* old_path, const char* new_path);
|
|
/* Remove the whole directory tree at `path` (confined, symlink-safe). Used by
|
|
--force to clear a non-empty destination directory that blocks an incoming
|
|
regular file. See the .c for the exact success semantics. */
|
|
bool file_remove_tree_secure(const char* path);
|
|
/* Open a private 0700 directory (creating it on demand) that must live below
|
|
the authorized root. Used for the --delay-updates staging directory. */
|
|
int file_open_private_dir(const char* dir_path);
|
|
|
|
/* Open an existing --temp-dir scratch directory (relative or absolute; no
|
|
creation). When an authorized receive root is configured the directory's
|
|
REAL path (symlinks resolved) must lie within it, so a client-planted
|
|
symlink cannot redirect receiver scratch files outside the sandbox; an
|
|
in-root symlink to another filesystem is still allowed for rsync's EXDEV
|
|
fallback. */
|
|
int file_open_temp_dir(const char* dir_path);
|
|
|
|
/* The file_to_disk_secure* variants write a temporary copy in the destination
|
|
directory and atomically rename it over `path`. temp_dir is a scratch
|
|
directory (an absolute path, or one the caller already resolved against the
|
|
destination root): when it is non-NULL the temporary copy is instead created
|
|
there (with a name unique across the whole scratch directory) and atomically
|
|
renamed into the destination directory once fully written and fsynced. When
|
|
that rename/link fails with EXDEV (the scratch dir is on another filesystem)
|
|
the write falls back to a non-atomic copy directly in the destination
|
|
directory, matching rsync. Pass NULL for the same-directory behavior.
|
|
--inplace writes never use temp_dir. */
|
|
bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size,
|
|
bool inplace, bool sparse, bool preallocate, const FileMetadata* metadata,
|
|
FileAttrPolicy policy, const char* temp_dir);
|
|
bool file_to_disk_secure_with_fsync(const char* path, const void* data,
|
|
unsigned long long data_size, bool inplace, bool sparse,
|
|
bool preallocate, const FileMetadata* metadata,
|
|
FileAttrPolicy policy, bool use_fsync, const char* temp_dir);
|
|
/* With update enabled, an existing newer destination is left untouched. The
|
|
check is descriptor-based for inplace writes; atomic replacement still has
|
|
an unavoidable final rename race without filesystem locking. */
|
|
bool file_to_disk_secure_update(const char* path, const void* data, unsigned long long data_size,
|
|
bool inplace, bool sparse, bool preallocate,
|
|
const FileMetadata* metadata, FileAttrPolicy policy,
|
|
const char* temp_dir);
|
|
bool file_to_disk_secure_no_replace(const char* path, const void* data,
|
|
unsigned long long data_size, bool sparse, bool preallocate,
|
|
const FileMetadata* metadata, FileAttrPolicy policy,
|
|
const char* temp_dir);
|
|
/* Receiver write-path variant that also applies per-file xattrs (-X/-A) and the
|
|
* --fake-super stat xattr fd-relative before the final rename. `update` /
|
|
* `no_replace` / `use_fsync` mirror the plain wrappers above; `keep_partial`
|
|
* enables --partial best-effort retention of a failed write's temp. */
|
|
bool file_to_disk_secure_attrs(const char* path, const void* data, unsigned long long data_size,
|
|
bool inplace, bool sparse, bool preallocate,
|
|
const FileMetadata* metadata, FileAttrPolicy policy, bool update,
|
|
bool no_replace, bool use_fsync, const FileXattrList* xattrs,
|
|
bool fake_super, bool keep_partial, const char* temp_dir);
|
|
/* Atomic --link-dest install: replace `path` with a hard link to `basis_path`
|
|
(via a temp name + rename); fall back to a byte-identical local copy from
|
|
`data` when the link is impossible (EXDEV/EPERM/unsupported filesystem).
|
|
`metadata` is applied only on the copy fallback. `preallocate` applies to
|
|
that copy fallback only (a hard-linked file shares the basis inode and is
|
|
never re-allocated). */
|
|
bool file_to_disk_secure_link(const char* path, const char* basis_path, const void* data,
|
|
unsigned long long data_size, bool preallocate,
|
|
const FileMetadata* metadata, FileAttrPolicy policy, bool use_fsync,
|
|
const char* temp_dir);
|
|
/* Like file_to_disk_secure_link, but the byte-copy fallback also applies the
|
|
* per-file xattrs (-X/-A) and --fake-super stat xattr (fd-relative). On a
|
|
* successful hard link no attributes are applied (the shared inode already
|
|
* carries the basis's). */
|
|
bool file_to_disk_secure_link_attrs(const char* path, const char* basis_path, const void* data,
|
|
unsigned long long data_size, bool preallocate,
|
|
const FileMetadata* metadata, FileAttrPolicy policy,
|
|
bool use_fsync, const FileXattrList* xattrs, bool fake_super,
|
|
const char* temp_dir);
|
|
/* Streaming --copy-dest install: atomically materialize `path` by copying the
|
|
* bytes of `basis_path` through a bounded buffer (no whole-file buffering, so
|
|
* an arbitrarily large basis works), applying the SOURCE metadata and the
|
|
* per-file xattrs / --fake-super record. `update` honors a newer destination;
|
|
* a --temp-dir scratch location falls back to a direct write on EXDEV. */
|
|
bool file_copy_basis_stream_attrs(const char* path, const char* basis_path,
|
|
unsigned long long expected_size, bool preallocate,
|
|
const FileMetadata* metadata, FileAttrPolicy policy, bool update,
|
|
bool use_fsync, const FileXattrList* xattrs, bool fake_super,
|
|
const char* temp_dir);
|
|
/* Protocol 2.28.0 receiver-stat variants: like the two above but additionally
|
|
* report through `dirs_created` (when non-NULL) how many parent directories the
|
|
* confined secure walk had to create that lie strictly below `count_floor` (a
|
|
* receive-root-relative prefix, or NULL for all). Used to reproduce rsync's
|
|
* `Number of created files` directory count on a fresh destination. */
|
|
bool file_to_disk_secure_attrs_counted(const char* path, const void* data,
|
|
unsigned long long data_size, bool inplace, bool sparse,
|
|
bool preallocate, const FileMetadata* metadata,
|
|
FileAttrPolicy policy, bool update, bool no_replace,
|
|
bool use_fsync, const FileXattrList* xattrs, bool fake_super,
|
|
bool keep_partial, const char* temp_dir,
|
|
unsigned* dirs_created, const char* count_floor);
|
|
bool file_to_disk_secure_link_attrs_counted(const char* path, const char* basis_path,
|
|
const void* data, unsigned long long data_size,
|
|
bool preallocate, const FileMetadata* metadata,
|
|
FileAttrPolicy policy, bool use_fsync,
|
|
const FileXattrList* xattrs, bool fake_super,
|
|
const char* temp_dir, unsigned* dirs_created,
|
|
const char* count_floor);
|
|
/* The logical transfer root expressed receive-root-relative, or NULL when the
|
|
* wire paths carry no mirror scaffolding above it. Caller frees non-NULL. */
|
|
char* file_transfer_root_floor(const Config* config);
|
|
|
|
#endif
|