#ifndef _GNU_SOURCE #define _GNU_SOURCE /* statx + STATX_BTIME for --crtimes birth-time capture */ #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "data.h" #include "checksum.h" #include "delta.h" #include "file.h" #include "file_store.h" #include "identity.h" #include "log.h" #include "metadata.h" #include "utils.h" #include "protocol.h" #include "xattr.h" #include #include /* Files larger than this are not loaded whole for transfer (the sender streams * them); a whole-file digest is computed from the path instead. Kept in sync * with the sender's streaming threshold. */ #define STREAM_THRESHOLD (64ULL * 1024 * 1024) static bool write_all(int fd, const void* data, unsigned long long size) { const unsigned char* p = data; unsigned long long done = 0; while (done < size) { ssize_t n = write(fd, p + done, (size_t)(size - done)); if (n < 0 && errno == EINTR) continue; if (n <= 0) return false; done += (unsigned long long)n; } return true; } /* Streaming copy of an open source descriptor into the just-created destination `fd` (already at offset 0). Used by the --copy-dest basis install so a basis larger than any in-memory whole-file bound still materializes without buffering the entire file. `expected_size` is the caller-verified basis size; the copy must produce exactly that many bytes (a short source is a hard error, never a silently truncated destination). The final ftruncate drops any residual tail a raced-in longer source might have left. */ static bool copy_fd_all(int dst_fd, int src_fd, unsigned long long expected_size) { unsigned char buf[1 << 20]; unsigned long long done = 0; while (done < expected_size) { unsigned long long remaining = expected_size - done; size_t want = remaining < sizeof(buf) ? (size_t)remaining : sizeof(buf); ssize_t n = read(src_fd, buf, want); if (n < 0 && errno == EINTR) continue; if (n <= 0) return false; if (!write_all(dst_fd, buf, (unsigned long long)n)) return false; done += (unsigned long long)n; } return ftruncate(dst_fd, (off_t)expected_size) == 0; } /* Preallocate `size` bytes on `fd` before any data is written (--preallocate). * fallocate(2) reserves real disk blocks, so an out-of-space condition * (ENOSPC/EDQUOT) surfaces up front instead of partway through a transfer; * unavoidable fragmentation of a streamed file is also reduced. rsync favors * the syscall over glibc posix_fallocate (whose emulation can be subtly * different), so try fallocate(2) first and only fall back to posix_fallocate, * then to ftruncate on filesystems (e.g. tmpfs, ZFS) that support neither. The * logical size is always extended, so the fail-fast/contiguity intent degrades * gracefully but never fails on an unsupported filesystem; genuine allocation * failures are propagated as the error code (caller fails the write). Neither * leaves the fd's file offset guaranteed, so the caller seeks back to 0 before * writing. Returns 0 on success (including the fallback) or a nonzero error * code. */ static int preallocate_fd(int fd, unsigned long long size) { if (size == 0) return 0; #ifdef __linux__ if (fallocate(fd, 0, 0, (off_t)size) == 0) return 0; if (errno != EOPNOTSUPP && errno != ENOSYS && errno != EINVAL) return errno; #endif int rc = posix_fallocate(fd, 0, (off_t)size); if (rc == EOPNOTSUPP || rc == ENOSYS) { if (ftruncate(fd, (off_t)size) == 0) return 0; return errno; } return rc; } /* Process-wide counter for scratch temp names. A --temp-dir scratch directory is flat: different destinations that share a basename must never race onto the same temp name. Deriving the trailing number from a global atomic sequence keeps every temp name unique across the whole scratch directory even when several threads write concurrently, so the O_EXCL creation loop below almost never needs a retry. */ static unsigned long long next_temp_sequence(void) { static atomic_ullong sequence; return atomic_fetch_add_explicit(&sequence, 1, memory_order_relaxed); } /* Process-wide umask, captured exactly once. Reading the umask requires a * get+set round trip (umask(0); umask(old)); doing that per write would be racy * in the multithreaded receiver, so the value is captured at process startup by * file_umask_capture() (called at the top of main(), before any threads exist). * The pthread_once fallback keeps a caller that never called the capture (e.g. a * unit test) correct. */ static unsigned g_process_umask; static atomic_bool g_process_umask_captured; static pthread_once_t g_process_umask_once = PTHREAD_ONCE_INIT; static void file_capture_umask_now(void) { mode_t mask = umask(0); umask(mask); g_process_umask = (unsigned)mask; atomic_store_explicit(&g_process_umask_captured, true, memory_order_release); } static void file_capture_umask_once(void) { if (atomic_load_explicit(&g_process_umask_captured, memory_order_acquire)) return; file_capture_umask_now(); } /* Re-captures the umask. Must only be called while the process is still * single-threaded (startup, or the daemon's post-fork setup after umask(0)), * so a later re-capture can refresh the cached value before any receiver * thread exists. */ void file_umask_capture(void) { file_capture_umask_now(); } unsigned file_process_umask(void) { if (!atomic_load_explicit(&g_process_umask_captured, memory_order_acquire)) pthread_once(&g_process_umask_once, file_capture_umask_once); return g_process_umask; } /* Base mode applied when the policy does not take the source mode wholesale * (i.e. --perms is off). A pre-existing destination keeps its own mode; a * brand-new file is created like rsync: source_mode & 0777 & ~umask (special * bits are not part of a mode-preserving transfer without -p). Only when no * metadata is available at all does the historical fixed 0644 default apply. * The -E rule (and no-op for a plain -t) is layered on top of this base. */ static mode_t file_mode_base(const FileMetadata* metadata, bool existing_known, mode_t existing_mode) { if (existing_known) return existing_mode; if (metadata) return metadata->mode & 0777 & ~(mode_t)file_process_umask(); return S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; } bool file_checksum(File* file, ChecksumAlgo algo, uint64_t seed, uint8_t* out, size_t out_capacity, size_t* out_len) { if (!file || !out || !out_len || !file->data) return false; if (file->data->size == 0) { return checksum_digest(algo, seed, "", 0, out, out_capacity, out_len); } /* A streamed source (data not loaded) may exceed any in-memory whole-file bound; hash it from the file path in bounded buffers instead of forcing a full load. This is the same digest the receiver recomputes on the basis. */ if (!file->data->data && file->path && file->data->size > STREAM_THRESHOLD && checksum_digest_file(algo, seed, file->path, out, out_capacity, out_len)) return true; if (!file->data->data && !file_load_data(file)) return false; return checksum_digest(algo, seed, file->data->data, file->data->size, out, out_capacity, out_len); } File* file_create(const char* path) { if (!path) return NULL; File* file = (File*)protocol_alloc(sizeof(File)); if (file == NULL) { log_perror("ERROR: Could not allocate memory for file struct"); return NULL; } size_t path_len = strlen(path); file->path = (char*)protocol_alloc(path_len + 1); if (file->path == NULL) { free(file); return NULL; } memcpy(file->path, path, path_len); file->path[path_len] = '\0'; file->send_path = NULL; file->data = data_create_reserve(0); if (file->data == NULL) { free(file->path); free(file); return NULL; } file->metadata = NULL; file->skip = false; file->is_dir = false; file->dir_time_only = false; file->basis_link = NULL; file->basis_copy = NULL; file->link_group = 0; file->link_first = false; file->hardlink_target = NULL; file->is_symlink = false; file->symlink_target = NULL; file->is_special = false; file->rdev_major = 0; file->rdev_minor = 0; file->xattrs = NULL; file->dest_state = (OutputDestState){0}; file->matched_bytes = 0; file->literal_bytes = 0; return file; } void file_destroy(void* item) { if (item == NULL) return; File* file = (File*)item; data_destroy(file->data); file->data = NULL; file_metadata_destroy(file->metadata); file->metadata = NULL; free(file->path); file->path = NULL; free(file->send_path); file->send_path = NULL; free(file->basis_link); file->basis_link = NULL; free(file->basis_copy); file->basis_copy = NULL; free(file->hardlink_target); file->hardlink_target = NULL; free(file->symlink_target); file->symlink_target = NULL; xattr_list_free(file->xattrs); file->xattrs = NULL; free(file); } FileMetadata* file_metadata_create(const char* path, const struct stat* stats, bool capture_atime, bool capture_crtime) { FileMetadata* m = protocol_alloc(sizeof(FileMetadata)); if (m == NULL) { log_perror("ERROR: Could not allocate memory for file metadata"); return NULL; } m->mode = stats->st_mode; m->uid = stats->st_uid; m->gid = stats->st_gid; m->mtime_sec = stats->st_mtime; #ifdef __linux__ m->mtime_nsec = stats->st_mtim.tv_nsec; #else m->mtime_nsec = 0; #endif /* -U/--atimes: capture the access time from the same pre-read stat the scanner already took, so the value is not clobbered by a later read for transfer. The timestamp is populated (and atime_valid set) only on Linux, where st_atim is populated; on other platforms the atime is left alone rather than clobbered to the default 0/epoch by an unpopulated value. */ #ifdef __linux__ m->atime_valid = capture_atime; m->atime_sec = stats->st_atim.tv_sec; m->atime_nsec = stats->st_atim.tv_nsec; #else m->atime_valid = false; m->atime_sec = 0; m->atime_nsec = 0; #endif /* -N/--crtimes: birth time is not available via struct stat in general; on Linux it needs statx STATX_BTIME. If unavailable it is captured as a documented no-op (the flag stays accepted, crtime_valid stays false). */ m->crtime_valid = false; m->crtime_sec = 0; m->crtime_nsec = 0; if (capture_crtime) { #ifdef STATX_BTIME struct statx stx; if (path != NULL && statx(AT_FDCWD, path, AT_STATX_SYNC_AS_STAT, STATX_BTIME, &stx) == 0 && (stx.stx_mask & STATX_BTIME) != 0) { m->crtime_valid = true; m->crtime_sec = (time_t)stx.stx_btime.tv_sec; m->crtime_nsec = (long)stx.stx_btime.tv_nsec; } #endif } return m; } void file_metadata_destroy(void* metadata) { free(metadata); } /* --open-noatime: process-wide sender policy (client-only, never crosses the * wire). When enabled, opening a source file for transfer uses O_NOATIME so * the read does not bump the source's on-disk access time. It degrades safely * to a normal open where O_NOATIME is unavailable (not defined) or refused * (EPERM, because it needs CAP_FOWNER): the data path never silently changes, * only the atime-bump is skipped. */ static bool file_open_noatime = false; void file_set_open_noatime(bool enable) { file_open_noatime = enable; } bool file_get_open_noatime(void) { return file_open_noatime; } /* Open `path` read-only for transfer, honouring --open-noatime when set. */ int file_open_for_read(const char* path) { int flags = O_RDONLY; #ifdef O_NOATIME if (file_get_open_noatime()) flags |= O_NOATIME; #endif int fd = open(path, flags); #ifdef O_NOATIME if (fd < 0 && (flags & O_NOATIME)) fd = open(path, O_RDONLY); /* degrade safely on EPERM / unsupported fs */ #endif return fd; } bool file_load_data(File* file) { if (file == NULL || !file->data) return false; if (file->data->data == NULL) { if (file->data->size == 0) return true; file->data->data = protocol_alloc(file->data->size); if (file->data->data == NULL) { log_perror("Could not allocate memory for file data"); return false; } } size_t bytes_read = file_content_to_buffer(file); if (bytes_read != file->data->size) { log_message(LOG_LEVEL_ERROR, "Did not read expected amount of bytes from file"); free(file->data->data); file->data->data = NULL; file->data->size = 0; return false; } return true; } size_t file_content_to_buffer(File* file) { if (!file || !file->path || !file->data || (!file->data->data && file->data->size != 0)) return 0; int fd = file_open_for_read(file->path); if (fd < 0) { log_perror("Could not open the file!"); return 0; } FILE* file_pointer = fdopen(fd, "rb"); if (file_pointer == NULL) { close(fd); log_perror("Could not open the file!"); return 0; } size_t bytes_read = fread(file->data->data, 1, file->data->size, file_pointer); if (bytes_read != (size_t)file->data->size) { fclose(file_pointer); log_perror("Read unexpected number of bytes from File!"); return 0; } fclose(file_pointer); return bytes_read; } /* ---- Secure filesystem primitives ---- */ bool file_path_exists_secure(const char* path) { if (!path) return false; char* leaf = NULL; int parent_fd = file_open_secure_parent(path, &leaf, false); if (parent_fd < 0) return false; struct stat st; bool exists = fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) == 0; close(parent_fd); free(leaf); return exists; } bool file_stat_secure(const char* path, struct stat* st) { if (!path || !st) return false; char* leaf = NULL; int parent_fd = file_open_secure_parent(path, &leaf, false); if (parent_fd < 0) return false; bool exists = fstatat(parent_fd, leaf, st, AT_SYMLINK_NOFOLLOW) == 0 && S_ISREG(st->st_mode); close(parent_fd); free(leaf); return exists; } static bool stat_is_newer(const struct stat* st, const FileMetadata* metadata) { if (!st || !metadata) return false; #ifdef __linux__ long mtime_nsec = st->st_mtim.tv_nsec; #else long mtime_nsec = 0; #endif return st->st_mtime > metadata->mtime_sec || (st->st_mtime == metadata->mtime_sec && mtime_nsec > metadata->mtime_nsec); } bool file_destination_is_newer_secure(const char* path, const FileMetadata* metadata) { struct stat st; return file_stat_secure(path, &st) && stat_is_newer(&st, metadata); } /* --keep-dirlinks (-K) receiver process-wide policy: when set, a destination * path component that is itself a symlink to an in-root directory is followed * (used as that directory) instead of failing the O_NOFOLLOW walk. Only ever * honoured when the resolved target is a directory that stays beneath the * authorized root, so a malicious symlink can never redirect the write outside * it. Client of record is the server's receiver. */ static bool file_keep_dirlinks = false; void file_set_keep_dirlinks(bool enable) { file_keep_dirlinks = enable; } /* --trust-sender (Phase 5) receiver process-wide policy: when set, the receiver * trusts the sender's file list and skips its own redundant up-front re- * validation (empty/".." path rejection, escaping-symlink-target containment). * Kept OFF by default; the server's per-connection handler sets it once from the * received config before any receiver/writer threads start (each connection is * its own forked process, so this per-process value never bleeds across * connections). */ static bool file_trust_sender = false; void file_set_trust_sender(bool enable) { file_trust_sender = enable; } bool file_get_trust_sender(void) { return file_trust_sender; } /* rsync 3.4.1 unsafe_symlink(): true when `target` (the link's destination * string) points outside the transfer tree rooted at the symlink's own * location. `link_path` is the symlink's path relative to the top of the * transfer (including its name). This is a purely lexical test matching * rsync's util1.c: absolute/empty targets are always unsafe; leading "../" * components are counted against the symlink's own directory depth; a ".." * that would climb above the transfer root is unsafe. rsync 3.4.1 additionally * rejects any INTERNAL "/../" component and a trailing "/..". */ bool file_symlink_unsafe(const char* target, const char* link_path) { if (!target || target[0] == '\0' || target[0] == '/') return true; const char* rest = target; while (strncmp(rest, "../", 3) == 0) { rest += 3; while (*rest == '/') rest++; } if (strstr(rest, "/../") != NULL) return true; size_t target_len = strlen(target); if (target_len > 3 && strcmp(&target[target_len - 3], "/..") == 0) return true; int depth = 0; const char* name; const char* slash; const char* src = link_path ? link_path : ""; for (name = src; (slash = strchr(name, '/')) != NULL; name = slash + 1) { if (*name == '.' && (name[1] == '/' || (name[1] == '.' && name[2] == '/'))) { if (name[1] == '.') depth = 0; } else { depth++; } while (slash[1] == '/') slash++; } if (*name == '.' && name[1] == '.' && name[2] == '\0') depth = 0; for (name = target; (slash = strchr(name, '/')) != NULL; name = slash + 1) { if (*name == '.' && (name[1] == '/' || (name[1] == '.' && name[2] == '/'))) { if (name[1] == '.') { if (--depth < 0) return true; } } else { depth++; } while (slash[1] == '/') slash++; } if (*name == '.' && name[1] == '.' && name[2] == '\0') depth--; return depth < 0; } /* Strict lexical helper: true when `target` is relative (not absolute) and * contains no ".." component at all, so it can never escape the directory it * is created in. This is stricter than rsync's unsafe_symlink() (which allows * an in-tree ".."); the scanner/receiver use file_symlink_unsafe()/--safe-links * for rsync parity, and this helper is retained for callers that want the * ".."-free guarantee. */ bool file_symlink_target_contained(const char* target) { if (!target || target[0] == '\0' || target[0] == '/') return false; const char* p = target; while (*p) { const char* slash = strchr(p, '/'); size_t comp_len = slash ? (size_t)(slash - p) : strlen(p); if (comp_len == 2 && p[0] == '.' && p[1] == '.') return false; if (!slash) break; p = slash + 1; } return true; } /* Remove a leading symlink munge marker (if present); returns true when the * marker was stripped. `target` is a mutable NUL-terminated buffer. */ bool file_symlink_unmunge(char* target) { if (!target) return false; static const char* const marker = SYMLINK_MUNGE_PREFIX; size_t marker_len = strlen(marker); if (strncmp(target, marker, marker_len) != 0) return false; size_t rest = strlen(target + marker_len) + 1; memmove(target, target + marker_len, rest); return true; } /* Owned copy of `target` prefixed with SYMLINK_MUNGE_PREFIX (the receiver-side * --munge-links rewriting, matching rsync's receiver). Returns NULL on * allocation failure. */ char* file_symlink_munge(const char* target) { if (!target) return NULL; static const char* const marker = SYMLINK_MUNGE_PREFIX; size_t marker_len = strlen(marker); size_t target_len = strlen(target); char* out = malloc(marker_len + target_len + 1); if (!out) return NULL; memcpy(out, marker, marker_len); memcpy(out + marker_len, target, target_len + 1); return out; } /* Create a symlink at `path` pointing to `target`, confined below the * authorized root: the parent directory is opened with an O_NOFOLLOW fd walk * and the link is created with symlinkat so neither the destination chain nor * the target is ever followed. The final component is never dereferenced: an * existing non-directory entry at `path` is unlinked by name before the link is * placed; an existing directory there is left untouched (returns false, so a * caller can treat it as a collision). The link VALUE `target` is copied * verbatim, matching rsync -l (which stores absolute and ".."-bearing targets * as-is); target policy is the caller's job -- the scanner applies * --safe-links/--copy-unsafe-links, and the receiver applies --munge-links. * The PLACEMENT path is always confined below the authorized root. */ bool file_symlink_at_secure(const char* path, const char* target) { if (!path || !target || has_path_traversal(path)) return false; char* leaf = NULL; int parent_fd = file_open_secure_parent(path, &leaf, true); if (parent_fd < 0) return false; bool ok = false; struct stat st; bool exists = fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) == 0; if (exists && S_ISDIR(st.st_mode)) { /* A directory already at this path cannot be replaced atomically with a symlink without --force semantics; leave it and report the collision. */ ok = false; } else { if (exists && unlinkat(parent_fd, leaf, 0) != 0 && errno != ENOENT) goto out; ok = symlinkat(target, parent_fd, leaf) == 0; } out: close(parent_fd); free(leaf); return ok; } /* Open the directory named by canonical absolute `resolved`, which the caller * has already verified lies beneath `root` (the canonical authorized root). * Each component is opened relative to the authorized-root fd with O_NOFOLLOW, * so a directory swapped for a symlink after the realpath() check cannot * redirect the open outside the root -- the walk simply fails. This replaces * re-opening the absolute resolved path (TOCTOU). Returns an O_DIRECTORY fd, * or -1 (the root itself and any error are refused). */ static int open_dir_beneath_root(const char* resolved, const char* root) { size_t root_len = strlen(root); const char* rel = resolved + root_len; while (*rel == '/') rel++; if (*rel == '\0') return -1; int root_fd = utils_get_authorized_root_fd(); if (root_fd < 0) return -1; int fd = dup(root_fd); if (fd < 0) return -1; char* copy = str_dup(rel); if (!copy) { close(fd); return -1; } char* save = NULL; for (char* component = strtok_r(copy, "/", &save); component; component = strtok_r(NULL, "/", &save)) { if (strcmp(component, ".") == 0) continue; /* A canonical realpath() output never contains "." or ".."; refuse ".." defensively rather than let it climb toward the root. */ int next = strcmp(component, "..") == 0 ? -1 : openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (next < 0) { close(fd); free(copy); return -1; } close(fd); fd = next; } free(copy); return fd; } int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs) { return file_open_secure_parent_counted(path, leaf_out, create_dirs, NULL, NULL); } /* The logical transfer root expressed in the same coordinate as the secure * parent walk's `rel_buf` (relative to the authorized root, with a leading * '/'), used as the floor at or below which a created directory is a real * file-list entry. The on-disk transfer root is the receive root joined to the * wire path; the mirror scaffolding above it (the absolute source path below * the destination root) is not an rsync entry. Returns an allocated string or * NULL (count every created component). */ static char* transfer_root_floor(const Config* config) { if (!config || !config->send_directory || config->send_directory[0] == '\0') return NULL; const char* spec = config->send_directory; const char* after = spec; if (spec[0] == '.' && spec[1] == '/') { after = spec + 2; } else { const char* cut = strstr(spec, "/./"); if (cut) after = cut + 3; } while (*after == '/') after++; char* wire_root = str_dup(after); if (!wire_root) return NULL; size_t wlen = strlen(wire_root); while (wlen > 0 && wire_root[wlen - 1] == '/') wire_root[--wlen] = '\0'; if (wlen == 0) { free(wire_root); return NULL; } char* disk_root = config->receive_root_directory ? path_cat(config->receive_root_directory, wire_root) : str_dup(wire_root); free(wire_root); if (!disk_root) return NULL; const char* root_path = utils_get_authorized_root_path(); const char* floor = disk_root; if (root_path && root_path[0] == '/') { size_t rl = strlen(root_path); while (rl > 0 && root_path[rl - 1] == '/') rl--; if (strncmp(disk_root, root_path, rl) == 0 && (disk_root[rl] == '/' || disk_root[rl] == '\0')) floor = disk_root + rl; } while (*floor == '/') floor++; char* out = str_dup(floor); free(disk_root); if (!out) return NULL; if (out[0] == '\0') { free(out); return NULL; } return out; } /* A created parent component counts toward `Number of created files` only when * its receive-root-relative path is at or below the logical transfer root * (`count_floor`). The transfer root itself corresponds to rsync's `.` entry * (created on a fresh destination, pre-existing otherwise); the mirror * scaffolding above it is FastSync's absolute-path layout, not an rsync entry. */ static bool created_dir_counts(const char* count_floor, const char* rel_buf, const char* component) { if (!count_floor) return true; char candidate[PATH_MAX]; int n = snprintf(candidate, sizeof(candidate), "%s/%s", rel_buf, component); if (n < 0 || (size_t)n >= sizeof(candidate)) return false; const char* cand = candidate; while (*cand == '/') cand++; size_t fl = strlen(count_floor); if (strncmp(cand, count_floor, fl) != 0) return false; return cand[fl] == '\0' || cand[fl] == '/'; } /* Public wrapper for the receiver's created-directory accounting: the logical * transfer root expressed receive-root-relative, or NULL when the wire paths * carry no mirror scaffolding above it (--relative and --files-from, whose * paths are already relative to the transfer root). The caller frees a * non-NULL result. */ char* file_transfer_root_floor(const Config* config) { if (!config || config->relative || config->files_from_set != NULL) return NULL; return transfer_root_floor(config); } int file_open_secure_parent_counted(const char* path, char** leaf_out, bool create_dirs, unsigned* dirs_created, const char* count_floor) { char* copy = str_dup(path); if (!copy) return -1; char* parent = dirname(copy); const char* slash = strrchr(path, '/'); char* leaf = str_dup(slash ? slash + 1 : path); if (!leaf) { free(copy); return -1; } int fd; int root_fd = utils_get_authorized_root_fd(); const char* root_path = utils_get_authorized_root_path(); if (root_fd >= 0) { if (!root_path || path[0] != '/' || !path_is_within_root(root_path, path)) { free(copy); free(leaf); return -1; } fd = dup(root_fd); if (fd < 0) { free(copy); free(leaf); return -1; } size_t root_len = strlen(root_path); char* relative = str_dup(path + root_len); if (!relative) { free(copy); free(leaf); close(fd); return -1; } free(copy); copy = relative; parent = dirname(copy); } else { fd = (parent[0] == '/') ? open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) : open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); } if (fd < 0) { free(copy); free(leaf); return -1; } char* save = NULL; char* component = strtok_r(parent, "/", &save); char rel_buf[PATH_MAX] = ""; while (component) { if (strcmp(component, "..") == 0) { close(fd); free(copy); free(leaf); return -1; } if (strcmp(component, ".") != 0) { int next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (next < 0 && create_dirs && errno == ENOENT) { bool created = mkdirat(fd, component, (mode_t)(0777 & ~(mode_t)file_process_umask())) == 0; if (created || errno == EEXIST) { /* Protocol 2.28.0: only directories the logical file list would create count toward `Number of created files`; the mirror scaffolding above the transfer root (e.g. the absolute source path under the destination root) is not an rsync entry. `count_floor` is a receive-root-relative prefix that must be reached before a created component is counted. */ if (created && dirs_created && created_dir_counts(count_floor, rel_buf, component)) (*dirs_created)++; /* P7 Wave E: --copy-as owns EVERY entry, including the intermediate directories this walk creates implicitly. Its target ids are a global policy, so they are available here without per-entry source metadata. Only a directory this walk actually created is chowned (a pre-existing destination directory is left alone, matching rsync's transferred-entry scope); the helper is a no-op unless an identity policy is active. */ if (created && identity_copy_as_active() && !identity_apply_ownership_link(fd, component, 0, 0)) { /* A REQUIRED --copy-as ownership that cannot be applied to a directory this walk just created must fail the entry rather than leave that implicit parent owned by the receiver. Preserve the failing errno across the cleanup so the caller logs the real reason. */ int saved_errno = errno; close(fd); free(copy); free(leaf); errno = saved_errno; return -1; } next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); } } /* --keep-dirlinks (-K): a path component that is an existing symlink to an in-root directory is used as THAT directory rather than failing the O_NOFOLLOW walk. Only honoured when the symlink resolves to a directory that stays beneath the authorized root, so a malicious link can never redirect the write outside it. */ if (next < 0 && file_keep_dirlinks && root_path != NULL && (errno == ELOOP || errno == ENOTDIR || errno == EACCES)) { struct stat lst; if (fstatat(fd, component, &lst, AT_SYMLINK_NOFOLLOW) == 0 && S_ISLNK(lst.st_mode)) { char candidate[PATH_MAX]; char root[PATH_MAX]; if (realpath(root_path, root) && snprintf(candidate, sizeof(candidate), "%s%s/%s", root, rel_buf, component) < (int)sizeof(candidate)) { char resolved[PATH_MAX]; if (realpath(candidate, resolved) && strcmp(resolved, root) != 0 && strncmp(root, resolved, strlen(root)) == 0 && (resolved[strlen(root)] == '/' || resolved[strlen(root)] == '\0')) { struct stat rst; if (stat(resolved, &rst) == 0 && S_ISDIR(rst.st_mode)) { /* Open the resolved directory through a relative no-follow walk from the authorized-root fd instead of re-opening the absolute `resolved` path: swapping an intermediate directory for a symlink between realpath() and open() (TOCTOU) then merely fails the walk rather than redirecting the fd outside the root. */ next = open_dir_beneath_root(resolved, root); } } } } } if (next < 0) { close(fd); free(copy); free(leaf); return -1; } close(fd); fd = next; /* Track the walked relative prefix so the -K candidate path can be reconstructed. An overflow while building it means the whole path is at the PATH_MAX edge, so fail hard rather than silently building a wrong (truncated) candidate for a later -K follow. */ size_t need = strlen(rel_buf) + strlen(component) + 2; if (need <= sizeof(rel_buf)) { strcat(rel_buf, "/"); strcat(rel_buf, component); } else if (file_keep_dirlinks) { close(fd); free(copy); free(leaf); return -1; } } component = strtok_r(NULL, "/", &save); } free(copy); *leaf_out = leaf; return fd; } /* Normalized copy of a directory path: leading '/' kept, trailing '/' removed * ("/" and "//" both collapse to "/"). A trailing slash otherwise makes the * last path component empty, so probing that empty leaf below its parent * always fails. */ static char* normalize_directory_path(const char* path) { if (!path) return NULL; size_t len = strlen(path); while (len > 1 && path[len - 1] == '/') len--; char* norm = malloc(len + 1); if (!norm) return NULL; memcpy(norm, path, len); norm[len] = '\0'; return norm; } bool file_ensure_directory_secure(const char* path) { if (!path) return false; char* norm = normalize_directory_path(path); if (!norm) return false; /* The authorized root is already an open directory, and the filesystem root is always present: there is no final component left to create for them. */ const char* root_path = utils_get_authorized_root_path(); bool root_is_open = utils_get_authorized_root_fd() >= 0 && root_path && strcmp(norm, root_path) == 0; if (root_is_open || strcmp(norm, "/") == 0) { free(norm); return true; } char* leaf = NULL; int parent_fd = file_open_secure_parent(norm, &leaf, true); free(norm); if (parent_fd < 0) return false; int dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); bool created = false; if (dir_fd < 0 && errno == ENOENT) { if (mkdirat(parent_fd, leaf, (mode_t)(0777 & ~(mode_t)file_process_umask())) == 0) { created = true; dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); } else if (errno == EEXIST) { dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); } } else if (dir_fd < 0 && errno == ENOTDIR) { /* rsync replaces a destination non-directory (regular file) with an incoming directory. Confined to the already-opened secure parent fd: the leaf is unlinked by name (never followed) and only a non-directory is ever removed, so this cannot escape the authorized root or remove a pre-existing directory tree. A symlink is left alone (openat with O_NOFOLLOW reports ELOOP, which takes no branch here), since replacing it is not required for FastSync's transferred directories and keeps --keep-dirlinks semantics untouched. */ struct stat leaf_st; if (fstatat(parent_fd, leaf, &leaf_st, AT_SYMLINK_NOFOLLOW) == 0 && !S_ISDIR(leaf_st.st_mode) && !S_ISLNK(leaf_st.st_mode)) { if (unlinkat(parent_fd, leaf, 0) == 0) { if (mkdirat(parent_fd, leaf, (mode_t)(0777 & ~(mode_t)file_process_umask())) == 0) created = true; /* On failure dir_fd stays < 0 below, so the caller still sees it. */ dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); } } } bool ok = dir_fd >= 0; /* --copy-as owns a directory this call just created (the final component; intermediate components were handled by file_open_secure_parent above). A failed REQUIRED ownership fails the call rather than leaving the directory owned by the receiver. */ if (ok && created && identity_copy_as_active() && !identity_apply_ownership_link(parent_fd, leaf, 0, 0)) ok = false; if (dir_fd >= 0) close(dir_fd); close(parent_fd); free(leaf); return ok; } /* True when `path` resolves to an existing directory below the authorized root * (never creating anything). Used by the server to decide whether a client's * destination root already exists. A trailing slash on `path` and a destination * equal to the authorized root itself are normalized/handled here so both * previously-working destination forms keep working. */ bool file_directory_exists_secure(const char* path) { if (!path) return false; char* norm = normalize_directory_path(path); if (!norm) return false; const char* root_path = utils_get_authorized_root_path(); bool root_is_open = utils_get_authorized_root_fd() >= 0 && root_path && strcmp(norm, root_path) == 0; if (root_is_open || strcmp(norm, "/") == 0) { free(norm); return true; } char* leaf = NULL; int parent_fd = file_open_secure_parent(norm, &leaf, false); free(norm); if (parent_fd < 0) return false; int dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (dir_fd < 0 && errno == ENOENT) dir_fd = -1; bool ok = dir_fd >= 0; if (dir_fd >= 0) close(dir_fd); close(parent_fd); free(leaf); return ok; } bool file_rename_secure(const char* old_path, const char* new_path) { char *old_leaf = NULL, *new_leaf = NULL; int old_parent = file_open_secure_parent(old_path, &old_leaf, false); int new_parent = file_open_secure_parent(new_path, &new_leaf, true); bool ok = old_parent >= 0 && new_parent >= 0 && renameat(old_parent, old_leaf, new_parent, new_leaf) == 0; if (old_parent >= 0) close(old_parent); if (new_parent >= 0) close(new_parent); free(old_leaf); free(new_leaf); return ok; } /* Recursively delete every entry inside an open directory, never following a symlink (an O_NOFOLLOW fd walk, so a symlink planted inside the tree can never redirect removal outside of it). The directory itself is left in place; returns false on any failure. */ static bool wipe_dir_fd(int dirfd) { int scanfd = dup(dirfd); if (scanfd < 0) return false; DIR* dir = fdopendir(scanfd); if (!dir) { close(scanfd); return false; } bool operation_ok = true; const struct dirent* entry; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; struct stat st; if (fstatat(dirfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0) { if (errno != ENOENT) operation_ok = false; continue; } if (S_ISDIR(st.st_mode)) { int childfd = openat(dirfd, entry->d_name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); bool child_removed = false; if (childfd >= 0) { child_removed = wipe_dir_fd(childfd); close(childfd); } else if (errno != ENOENT) { operation_ok = false; } if (child_removed && unlinkat(dirfd, entry->d_name, AT_REMOVEDIR) != 0 && errno != ENOENT) operation_ok = false; } else { /* Files and symlinks alike are removed by name, never followed. */ if (unlinkat(dirfd, entry->d_name, 0) != 0 && errno != ENOENT) operation_ok = false; } } closedir(dir); return operation_ok; } /* Remove the whole directory tree at `path` (confined below the authorized root, symlink-safe). --force uses this to clear a non-empty destination directory that blocks an incoming regular file. Returns true when the path no longer exists as a directory (a missing path or a non-directory at the final component is a no-op success; the normal write path replaces files). */ bool file_remove_tree_secure(const char* path) { if (!path) return false; char* leaf = NULL; int parent_fd = file_open_secure_parent(path, &leaf, false); if (parent_fd < 0) return false; int dirfd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (dirfd < 0) { bool absent = errno == ENOENT || errno == ENOTDIR || errno == ELOOP; close(parent_fd); free(leaf); return absent; } bool ok = wipe_dir_fd(dirfd); close(dirfd); if (ok && unlinkat(parent_fd, leaf, AT_REMOVEDIR) != 0 && errno != ENOENT) ok = false; close(parent_fd); free(leaf); return ok; } /* Open a private staging/scratch directory, creating it (and any missing path components) on demand. dir_path is expected to already be confined below the authorized root by the caller; file_open_secure_parent re-checks that confinement and rejects `..` components, so a scratch directory can never be created or opened outside the destination root. The directory itself is created 0700 so other users cannot race on names inside it. Returns an O_DIRECTORY|O_NOFOLLOW fd, or -1 on error. */ int file_open_private_dir(const char* dir_path) { if (!dir_path) return -1; char* leaf = NULL; int parent_fd = file_open_secure_parent(dir_path, &leaf, true); if (parent_fd < 0) return -1; int fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (fd < 0 && errno == ENOENT) { if (mkdirat(parent_fd, leaf, 0700) == 0 || errno == EEXIST) fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); } close(parent_fd); free(leaf); return fd; } /* Open a --temp-dir scratch directory exactly as rsync does: the directory must * already exist and is used as given (an absolute path is used verbatim, a * relative one was already resolved against the destination root by the * caller). Unlike file_open_private_dir this neither creates it nor confines * it below the receive root, because rsync accepts any temp dir -- including * one outside the destination tree or on another filesystem. Returns an * O_DIRECTORY|O_CLOEXEC fd, or -1 on error. */ int file_open_temp_dir(const char* dir_path) { if (!dir_path) return -1; return open(dir_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); } /* After the content and mode/times are restored on the just-written file, apply * the per-file xattrs (-X/-A) and, for --fake-super, park the source's * uid/gid/mode/mtime in the reserved xattr. All fd-relative (confined to the * destination file) and best-effort: a per-attribute or privilege failure is * logged and skipped, never fatal. */ static void restore_extra_fd(int fd, const FileMetadata* metadata, const FileXattrList* xattrs, bool fake_super, FileAttrPolicy policy) { xattr_apply_fd(fd, xattrs); if (fake_super && metadata) { /* Record the ownership that WOULD have been applied: when an explicit ownership request (--chown/--usermap/--groupmap/--copy-as or -o/-g) is active, the resolved mapping; otherwise the source's own id. The real chown is suppressed (identity_apply_ownership early-returns under --fake-super) so recording never defeats the flag. Mode/mtime are still replayed (policy-gated) so unprivileged --fake-super keeps working. */ uint32_t store_uid; uint32_t store_gid; identity_resolve_storage_ids((int32_t)metadata->uid, (int32_t)metadata->gid, &store_uid, &store_gid); fake_super_store_fd(fd, store_uid, store_gid, (uint32_t)metadata->mode, metadata->mtime_sec, metadata->mtime_nsec); fake_super_restore_fd(fd, policy); } } static bool file_to_disk_secure_impl(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 char* temp_dir, const FileXattrList* xattrs, bool fake_super, bool keep_partial, unsigned* dirs_created, const char* count_floor) { char* leaf = NULL; int dirfd = file_open_secure_parent_counted(path, &leaf, true, dirs_created, count_floor); if (dirfd < 0) return false; int fd = -1; bool ok = false; /* Set when a --temp-dir install fails with EXDEV: rsync then falls back to a * non-atomic write directly in the destination directory (see the tail of * this function). */ bool cross_device_fallback = false; /* The base mode applied when --perms is off (neither the source mode nor an * exec-only change is taken wholesale): a pre-existing destination keeps its * own mode (special bits dropped), while a brand-new file uses * source&~umask when metadata is available (see file_mode_base) or 0644 when * there is none. Captured from the destination probe before the write. */ mode_t existing_mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; bool existing_mode_known = false; if (inplace) { /* --inplace writes directly into the destination; a scratch --temp-dir does not apply and must never redirect these writes. */ /* Type gate BEFORE opening: an existing destination entry that is not a regular file (FIFO, socket, char/block device, directory) must never be opened for writing. Opening a FIFO would block the receive thread forever and writing into a device would bypass the --write-devices / super-mode gate (a client-controlled device write). fstatat with AT_SYMLINK_NOFOLLOW does not follow a symlink and does not block. */ struct stat pre_stat; if (fstatat(dirfd, leaf, &pre_stat, AT_SYMLINK_NOFOLLOW) == 0) { if (!S_ISREG(pre_stat.st_mode)) { close(dirfd); free(leaf); return false; } /* Capture the old destination mode before the overwrite so a no--p/-E * write can restore it (the write itself may clear setuid/setgid). */ existing_mode = pre_stat.st_mode & 0777; existing_mode_known = true; } /* O_NONBLOCK: a no-op for a regular file, but a raced-in FIFO cannot block the open before the post-open S_ISREG re-check rejects it. */ fd = openat(dirfd, leaf, O_WRONLY | O_CREAT | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK, 0644); if (fd >= 0) { struct stat destination_stat; /* Re-check the opened descriptor: a concurrent replacement between the fstatat probe and the open (or a device/FIFO raced in) must never be written through. */ if (fstat(fd, &destination_stat) != 0 || !S_ISREG(destination_stat.st_mode)) { close(fd); close(dirfd); free(leaf); return false; } bool newer = false; if (update && metadata && stat_is_newer(&destination_stat, metadata)) { newer = true; } if (newer) { ok = true; } else { /* Preallocate the expected payload size before writing so an out-of-space condition fails cleanly up front (--preallocate). rsync lets --preallocate win over --sparse (the reserved blocks survive the sparse writer's seeks), so both flags can be active. */ int prealloc_rc = 0; if (preallocate && data_size > 0) { prealloc_rc = preallocate_fd(fd, data_size); if (prealloc_rc != 0) { char* escaped_path = output_escape(path, log_get_8_bit_output()); log_message(LOG_LEVEL_ERROR, "preallocate failed for '%s' (%s); transfer aborted", escaped_path ? escaped_path : "", strerror(prealloc_rc)); free(escaped_path); } } if (prealloc_rc == 0) { /* posix_fallocate does not guarantee the fd's file offset is left unchanged, so seek back to 0 before the data write. */ lseek(fd, 0, SEEK_SET); if (sparse && data_size > 0) ok = ftruncate(fd, (off_t)data_size) == 0; if (ok || !sparse || data_size == 0) ok = sparse && data_size > 0 ? file_store_write_sparse(fd, (const unsigned char*)data, data_size) : write_all(fd, data, data_size); if (ok) ok = ftruncate(fd, (off_t)data_size) == 0; /* Normalize the mode: apply the metadata-derived safe mode when the sender supplied metadata (setuid/setgid/sticky are never honored); otherwise fall back to a safe default so dangerous bits on an existing destination cannot survive an overwrite. When the policy requests neither -p nor -E the source mode is deliberately ignored and the pre-existing destination mode (or 0644 for a new file) is restored instead. The exec-bits-only -E change is likewise applied on top of that destination-derived base, not the scratch file's 0600. */ if (ok) { if (metadata) { if (!policy.perms && fchmod(fd, file_mode_base(metadata, existing_mode_known, existing_mode)) != 0) ok = false; if (ok) ok = file_restore_metadata_fd(fd, metadata, policy); } else if (fchmod(fd, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) != 0) { ok = false; } } if (ok) restore_extra_fd(fd, metadata, xattrs, fake_super, policy); if (ok && use_fsync) ok = fsync(fd) == 0; } } } } else { /* The --update newer-destination check runs first so a skipped file never creates an empty scratch directory behind it. */ /* True once the temp is being written: distinguishes a mid-write/metadata/ install failure (partial data may exist, --partial may retain it) from a pre-write validation failure (nothing to retain). */ bool write_attempted = false; /* Probe the destination ONCE up front: it both drives the --update check and records the pre-existing mode the no--p/-E fallback preserves. */ struct stat destination_stat; bool destination_is_regular = fstatat(dirfd, leaf, &destination_stat, AT_SYMLINK_NOFOLLOW) == 0 && S_ISREG(destination_stat.st_mode); if (destination_is_regular) { existing_mode = destination_stat.st_mode & 0777; existing_mode_known = true; } if (update && metadata && destination_is_regular && stat_is_newer(&destination_stat, metadata)) { close(dirfd); free(leaf); return true; } /* Scratch directory for the temporary working copy. When NULL the temp file is created in the destination directory, exactly as historically. */ int scratch_dirfd = -1; if (temp_dir) { scratch_dirfd = file_open_temp_dir(temp_dir); if (scratch_dirfd < 0) { int saved_errno = errno; log_message(LOG_LEVEL_ERROR, "--temp-dir '%s' could not be opened (rsync requires it to already exist): %s", temp_dir, strerror(saved_errno)); close(dirfd); free(leaf); return false; } } /* Temp names can exceed NAME_MAX for basenames near the limit (leaf plus the ".tmp.." decoration); heap-size the buffer instead of truncating into a fixed array, which would silently collide in a flat scratch directory. The sizing sentinel is the widest value of each format. */ int tmp_size; if (scratch_dirfd >= 0) tmp_size = snprintf(NULL, 0, ".%s.tmp.%ld.%llu", leaf, (long)getpid(), ~0ULL); else tmp_size = snprintf(NULL, 0, ".%s.tmp.%ld.%u", leaf, (long)getpid(), 999U); if (tmp_size < 0) { if (scratch_dirfd >= 0) close(scratch_dirfd); close(dirfd); free(leaf); return false; } char* tmp = malloc((size_t)tmp_size + 1); if (!tmp) { if (scratch_dirfd >= 0) close(scratch_dirfd); close(dirfd); free(leaf); return false; } for (unsigned int i = 0; i < 100; ++i) { /* The temp name is created inside the scratch directory (when one is configured) and, on success, atomically renamed into the destination directory. In a shared scratch directory the atomic sequence number keeps the name unique even for destinations with a common basename. */ if (scratch_dirfd >= 0) snprintf(tmp, (size_t)tmp_size + 1, ".%s.tmp.%ld.%llu", leaf, (long)getpid(), next_temp_sequence()); else snprintf(tmp, (size_t)tmp_size + 1, ".%s.tmp.%ld.%u", leaf, (long)getpid(), i); fd = openat(scratch_dirfd >= 0 ? scratch_dirfd : dirfd, tmp, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600); if (fd < 0) continue; /* EEXIST (or a transient open error): try a fresh name. */ int prealloc_rc = 0; if (preallocate && data_size > 0) { prealloc_rc = preallocate_fd(fd, data_size); if (prealloc_rc != 0) { char* escaped_path = output_escape(path, log_get_8_bit_output()); log_message(LOG_LEVEL_ERROR, "preallocate failed for '%s' (%s); transfer aborted", escaped_path ? escaped_path : "", strerror(prealloc_rc)); free(escaped_path); } } if (prealloc_rc == 0) { lseek(fd, 0, SEEK_SET); if (sparse && data_size > 0) ok = ftruncate(fd, (off_t)data_size) == 0; /* A real write attempt begins here (the ftruncate presize succeeded or no presize applies): a later mid-write / metadata / fsync / install failure may leave partial data that --partial retention can rename. */ if (ok || (!sparse || data_size == 0)) { write_attempted = true; ok = sparse && data_size > 0 ? file_store_write_sparse(fd, (const unsigned char*)data, data_size) : write_all(fd, data, data_size); } if (ok) { if (metadata) { if (!policy.perms && fchmod(fd, file_mode_base(metadata, existing_mode_known, existing_mode)) != 0) ok = false; if (ok) ok = file_restore_metadata_fd(fd, metadata, policy); } else if (fchmod(fd, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) != 0) { ok = false; } } if (ok) restore_extra_fd(fd, metadata, xattrs, fake_super, policy); if (ok && use_fsync) ok = fsync(fd) == 0; } if (close(fd) != 0) ok = false; fd = -1; if (ok) { if (no_replace) { /* The probe and commit cannot be one operation. A concurrent creator may win; EEXIST is then the requested skip. */ if (linkat(scratch_dirfd >= 0 ? scratch_dirfd : dirfd, tmp, dirfd, leaf, 0) == 0 || errno == EEXIST) { if (unlinkat(scratch_dirfd >= 0 ? scratch_dirfd : dirfd, tmp, 0) != 0 && errno != ENOENT) ok = false; } else { /* Cross-device (or otherwise impossible) link: rsync falls back to writing the file directly in the destination directory. Record it and retry below with no scratch dir. */ if (scratch_dirfd >= 0 && errno == EXDEV) cross_device_fallback = true; ok = false; } } else if (renameat(scratch_dirfd >= 0 ? scratch_dirfd : dirfd, tmp, dirfd, leaf) != 0) { if (scratch_dirfd >= 0 && errno == EXDEV) cross_device_fallback = true; ok = false; } } if (!ok) { /* --partial retention (best-effort): on a failure that happened after the temp held data (mid-write / metadata / fsync / install error), keep the already-written temp at the final destination path instead of unlinking it, so a later --append / --append-verify run can resume. This only ever renames the already-written temp (never a corrupt blend); the rename can fail (cross-device, permissions) and we then fall through to the normal unlink cleanup. Never retains when keep_partial is off, when nothing was actually written, or under --ignore-existing/--existing (no_replace), where the destination is not ours to overwrite. */ if (!keep_partial || !write_attempted || no_replace || renameat(scratch_dirfd >= 0 ? scratch_dirfd : dirfd, tmp, dirfd, leaf) != 0) unlinkat(scratch_dirfd >= 0 ? scratch_dirfd : dirfd, tmp, 0); } /* Once the temp fd was created the outcome is permanent: a write, metadata, fsync, close, linkat or renameat failure will not be fixed by retrying under a fresh name, so stop here. Only the open-failure path above retries a new name. */ break; } free(tmp); if (scratch_dirfd >= 0) close(scratch_dirfd); } if (fd >= 0) close(fd); close(dirfd); free(leaf); if (cross_device_fallback) { /* rsync semantics: a --temp-dir on another filesystem must not abort the write. Retry once with no scratch dir so the file is written and installed non-atomically in the destination directory. */ log_message(LOG_LEVEL_WARNING, "temp dir is on a different filesystem than the destination; falling back to a " "non-atomic copy into the destination directory"); return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata, policy, update, no_replace, use_fsync, NULL, xattrs, fake_super, keep_partial, dirs_created, count_floor); } return ok; } 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) { return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata, policy, false, false, false, temp_dir, NULL, false, false, NULL, NULL); } 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) { return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata, policy, true, false, false, temp_dir, NULL, false, false, NULL, NULL); } 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) { return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata, policy, false, false, use_fsync, temp_dir, NULL, false, false, NULL, NULL); } 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) { return file_to_disk_secure_impl(path, data, data_size, false, sparse, preallocate, metadata, policy, false, true, false, temp_dir, NULL, false, false, NULL, NULL); } /* Receiver write-path variant that also applies the per-file xattrs (-X/-A) * and, under --fake-super, parks the source stat in the reserved xattr, on the * just-written file descriptor before the final rename. `no_replace` / `update` * mirror the plain wrappers; `keep_partial` enables --partial retention of a * failed write's temp. See file_to_disk_secure_impl for the semantics. */ 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) { return file_to_disk_secure_attrs_counted(path, data, data_size, inplace, sparse, preallocate, metadata, policy, update, no_replace, use_fsync, xattrs, fake_super, keep_partial, temp_dir, NULL, NULL); } 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) { return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata, policy, update, no_replace, use_fsync, temp_dir, xattrs, fake_super, keep_partial, dirs_created, count_floor); } /* Atomic --link-dest install. The destination is replaced (via a temporary * name and a final rename) with a hard link to `basis_path`. When a hard * link cannot be created (the basis lives on a different filesystem, the * filesystem refuses hard links, ...) the install falls back to writing a * local copy from `data`/`data_size`, which the caller has already verified is * byte-identical to the basis file. `metadata` is only applied on that copy * fallback; a successful hard link keeps the basis inode's own attributes * (applying metadata through the shared inode would mutate the basis file). * Returns false only when both the link and the copy fallback fail. */ /* --link-dest / -H hardlink install with a byte-copy fallback. `metadata` is * applied only on the copy fallback; a successful hard link keeps the basis * inode's own attributes (applying through the shared inode would mutate the * basis). Likewise `xattrs`/`fake_super` are applied only on the copy * fallback, so a fallback copy preserves the per-file attributes instead of * silently dropping them. */ /* Streaming --copy-dest basis install: atomically materialize `path` from the * bytes of `basis_path` without holding the file in memory, so a basis larger * than any whole-file bound still works. Mirrors the ordinary secure store * path (confined parent walk, temp + rename, --update/--ignore-existing/ * --preallocate/--temp-dir) but sources the data from the basis descriptor * rather than a caller buffer, and applies the SOURCE metadata (rsync copies * then fixes attributes). A hard-link install that falls back to a byte copy * also routes through here when the caller supplies the basis path. */ static bool file_copy_basis_stream_impl(const char* path, const char* basis_path, unsigned long long expected_size, bool preallocate, const FileMetadata* metadata, FileAttrPolicy policy, bool update, bool no_replace, bool use_fsync, const FileXattrList* xattrs, bool fake_super, const char* temp_dir, unsigned* dirs_created, const char* count_floor) { if (!path || !basis_path) return false; char* leaf = NULL; int dirfd = file_open_secure_parent_counted(path, &leaf, true, dirs_created, count_floor); if (dirfd < 0) return false; char* basis_leaf = NULL; int basis_dirfd = file_open_secure_parent(basis_path, &basis_leaf, false); int src_fd = -1; if (basis_dirfd >= 0 && basis_leaf != NULL) { /* O_NONBLOCK rejects a raced-in FIFO without blocking; the S_ISREG gate below is the real type check. */ src_fd = openat(basis_dirfd, basis_leaf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK); struct stat src_st; if (src_fd >= 0 && (fstat(src_fd, &src_st) != 0 || !S_ISREG(src_st.st_mode))) { close(src_fd); src_fd = -1; } } if (basis_dirfd >= 0) close(basis_dirfd); free(basis_leaf); if (src_fd < 0) { close(dirfd); free(leaf); return false; } struct stat destination_stat; bool destination_is_regular = fstatat(dirfd, leaf, &destination_stat, AT_SYMLINK_NOFOLLOW) == 0 && S_ISREG(destination_stat.st_mode); if (update && metadata && destination_is_regular && stat_is_newer(&destination_stat, metadata)) { close(src_fd); close(dirfd); free(leaf); return true; } if (no_replace && file_path_exists_secure(path)) { close(src_fd); close(dirfd); free(leaf); return true; } int scratch_dirfd = -1; if (temp_dir) { scratch_dirfd = file_open_temp_dir(temp_dir); if (scratch_dirfd < 0) { int saved_errno = errno; log_message(LOG_LEVEL_ERROR, "--temp-dir '%s' could not be opened (rsync requires it to already exist): %s", temp_dir, strerror(saved_errno)); close(src_fd); close(dirfd); free(leaf); return false; } } int target_dirfd = scratch_dirfd >= 0 ? scratch_dirfd : dirfd; int tmp_size = snprintf(NULL, 0, ".%s.tmp.%ld.%llu", leaf, (long)getpid(), ~0ULL); char* tmp = NULL; bool ok = false; if (tmp_size >= 0) tmp = malloc((size_t)tmp_size + 1); if (tmp) { for (unsigned int i = 0; i < 100 && !ok; ++i) { if (scratch_dirfd >= 0) snprintf(tmp, (size_t)tmp_size + 1, ".%s.tmp.%ld.%llu", leaf, (long)getpid(), next_temp_sequence()); else snprintf(tmp, (size_t)tmp_size + 1, ".%s.tmp.%ld.%u", leaf, (long)getpid(), i); int fd = openat(target_dirfd, tmp, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600); if (fd < 0) { if (errno != EEXIST) break; continue; } bool wrote = true; if (preallocate && expected_size > 0 && preallocate_fd(fd, expected_size) != 0) wrote = false; if (wrote) wrote = copy_fd_all(fd, src_fd, expected_size); if (wrote && metadata) { if (!policy.perms && fchmod(fd, file_mode_base(metadata, destination_is_regular, destination_is_regular ? destination_stat.st_mode & 0777 : 0)) != 0) wrote = false; if (wrote) wrote = file_restore_metadata_fd(fd, metadata, policy); } else if (wrote && fchmod(fd, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) != 0) { wrote = false; } if (wrote) restore_extra_fd(fd, metadata, xattrs, fake_super, policy); if (wrote && use_fsync) wrote = fsync(fd) == 0; if (close(fd) != 0) wrote = false; if (wrote && renameat(target_dirfd, tmp, dirfd, leaf) != 0) wrote = false; if (!wrote) unlinkat(target_dirfd, tmp, 0); ok = wrote; } free(tmp); } if (!ok && scratch_dirfd >= 0) { /* Retry once with no scratch dir (rsync's EXDEV fallback). */ close(scratch_dirfd); close(src_fd); close(dirfd); free(leaf); return file_copy_basis_stream_impl(path, basis_path, expected_size, preallocate, metadata, policy, update, no_replace, use_fsync, xattrs, fake_super, NULL, dirs_created, count_floor); } if (scratch_dirfd >= 0) close(scratch_dirfd); close(src_fd); close(dirfd); free(leaf); return ok; } /* --copy-dest basis install (streaming). Applies the source metadata and the per-file xattrs / --fake-super record. */ 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) { return file_copy_basis_stream_impl(path, basis_path, expected_size, preallocate, metadata, policy, update, false, use_fsync, xattrs, fake_super, temp_dir, NULL, NULL); } static bool file_to_disk_secure_link_impl(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) { if (!path || !basis_path) return false; /* The caller-supplied buffer is no longer used: the copy fallback streams from the basis path (which may hold an over-limit file). Kept in the signature for the existing API. */ (void)data; char* leaf = NULL; int dirfd = file_open_secure_parent_counted(path, &leaf, true, dirs_created, count_floor); if (dirfd < 0) return false; int scratch_dirfd = -1; if (temp_dir) { scratch_dirfd = file_open_temp_dir(temp_dir); if (scratch_dirfd < 0) { int saved_errno = errno; log_message(LOG_LEVEL_ERROR, "--temp-dir '%s' could not be opened (rsync requires it to already exist): %s", temp_dir, strerror(saved_errno)); close(dirfd); free(leaf); return false; } } char* basis_leaf = NULL; int basis_dirfd = file_open_secure_parent(basis_path, &basis_leaf, false); bool linked = false; if (basis_dirfd >= 0 && basis_leaf != NULL) { int tmp_size = snprintf(NULL, 0, ".%s.tmp.%ld.%llu", leaf, (long)getpid(), ~0ULL); char* tmp = NULL; if (tmp_size >= 0) tmp = malloc((size_t)tmp_size + 1); if (!tmp) { log_message(LOG_LEVEL_ERROR, "memory allocation failed while hard-linking basis file"); } else { for (unsigned int i = 0; i < 100 && !linked; ++i) { if (scratch_dirfd >= 0) snprintf(tmp, (size_t)tmp_size + 1, ".%s.tmp.%ld.%llu", leaf, (long)getpid(), next_temp_sequence()); else snprintf(tmp, (size_t)tmp_size + 1, ".%s.tmp.%ld.%u", leaf, (long)getpid(), i); if (linkat(basis_dirfd, basis_leaf, scratch_dirfd >= 0 ? scratch_dirfd : dirfd, tmp, 0) == 0) { linked = true; break; } if (errno != EEXIST) break; /* EXDEV / EPERM / ...: give up and fall back to a copy */ } if (linked) { int target_dirfd = scratch_dirfd >= 0 ? scratch_dirfd : dirfd; if (use_fsync) { /* O_NONBLOCK: the freshly linked temp is normally the basis's regular file, but a raced-in FIFO at the name must not block this reopen forever. With O_NONBLOCK such an open fails with ENXIO instead of blocking, which is treated as a benign fsync-skip (the link itself is still installed); any other open/fsync failure falls back to the byte-copy path as before. */ int tfd = openat(target_dirfd, tmp, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK); if (tfd < 0) { if (errno != ENXIO) linked = false; } else if (fsync(tfd) != 0) { linked = false; close(tfd); } else { close(tfd); } } if (linked && renameat(target_dirfd, tmp, dirfd, leaf) != 0) linked = false; if (!linked) unlinkat(target_dirfd, tmp, 0); } free(tmp); } } if (basis_dirfd >= 0) close(basis_dirfd); free(basis_leaf); basis_leaf = NULL; if (!linked) { if (scratch_dirfd >= 0) close(scratch_dirfd); close(dirfd); free(leaf); /* The basis file could not be linked in (missing, cross-device, refused by the filesystem). Stream a byte-identical local copy from the basis itself (never the possibly-absent caller buffer) so an over-limit basis still materializes. When the basis path is not a readable regular file (e.g. a directory raced in), fall back to the caller-supplied bytes. */ if (file_copy_basis_stream_impl(path, basis_path, data_size, preallocate, metadata, policy, false, false, use_fsync, xattrs, fake_super, temp_dir, dirs_created, count_floor)) return true; return file_to_disk_secure_attrs_counted( path, data, data_size, false, false, preallocate, metadata, policy, false, false, use_fsync, xattrs, fake_super, false, temp_dir, dirs_created, count_floor); } if (scratch_dirfd >= 0) close(scratch_dirfd); close(dirfd); free(leaf); return true; } 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) { return file_to_disk_secure_link_impl(path, basis_path, data, data_size, preallocate, metadata, policy, use_fsync, NULL, false, temp_dir, NULL, NULL); } 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) { return file_to_disk_secure_link_attrs_counted(path, basis_path, data, data_size, preallocate, metadata, policy, use_fsync, xattrs, fake_super, temp_dir, NULL, NULL); } 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) { return file_to_disk_secure_link_impl(path, basis_path, data, data_size, preallocate, metadata, policy, use_fsync, xattrs, fake_super, temp_dir, dirs_created, count_floor); } bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size, bool inplace, bool sparse) { if (!path || (!data && data_size != 0) || has_path_traversal(path)) return false; FileAttrPolicy policy = {false, false, false, false}; return file_to_disk_secure(path, data, data_size, inplace, sparse, false, NULL, policy, NULL); }