#include #include #include #include #include #include #include #include #include #include #include #include "array_list.h" #include "charset.h" #include "chmod.h" #include "chunk.h" #include "compression.h" #include "config.h" #include "data.h" #include "delay_updates.h" #include "delta.h" #include "file.h" #include "file_receive.h" #include "format.h" #include "identity.h" #include "incremental_check.h" #include "log.h" #include "metadata.h" #include "protocol.h" #include "utils.h" #include "xattr.h" File* file_receive(const Config* config, int file_descriptor) { char* path = receive_wire_str(file_descriptor); if (path == NULL) return NULL; if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) { char* escaped_path = output_escape(path, log_get_8_bit_output()); log_message(LOG_LEVEL_ERROR, "Invalid received file path: %s", escaped_path ? escaped_path : ""); free(escaped_path); free(path); return NULL; } File* file = file_create(path); free(path); if (file == NULL) return NULL; if (config->use_metadata) { int meta_ok = 1; file->metadata = metadata_receive(file_descriptor, &meta_ok); if (!meta_ok) { file_destroy(file); return NULL; } } if (!receive_file_xattrs(file, file_descriptor, config)) { file_destroy(file); return NULL; } Data* file_data = receive_data_limited(file_descriptor, MAX_RECEIVE_WHOLE_FILE_SIZE); if (file_data == NULL) { file_destroy(file); return NULL; } if (config->use_compression && !compression_should_skip_with_suffixes(file->path, config->skip_compress_suffixes, config->skip_compress_set ? config->skip_compress_count : -1)) { Data* file_data_uncompressed = data_decompress_limited(file_data, MAX_RECEIVE_WHOLE_FILE_SIZE); ProtocolSession* owner = file_data->owner; data_destroy(file_data); if (file_data_uncompressed == NULL) { file_destroy(file); return NULL; } if (!data_charge_session(file_data_uncompressed, owner, file_data_uncompressed->size)) { data_destroy(file_data_uncompressed); file_destroy(file); return NULL; } if (file_data_uncompressed->size > MAX_FILE_DATA_SIZE) { data_destroy(file_data_uncompressed); file_destroy(file); return NULL; } file_data = file_data_uncompressed; } data_destroy(file->data); file->data = file_data; return file; } /* ---- P7 Wave D: deferred directory metadata ---- */ bool dir_metadata_should_capture(const Config* config) { /* Directory metadata is captured when a directory attribute is actually * requested: -p/--perms (directory modes), -t/--times (directory mtimes, * unless -O/--omit-dir-times suppresses them), -o/-g (directory ownership), * or -X/-A (directory xattrs/ACLs). --atimes/-U alone does not pull * directory metadata (matching the original dir-time bundle). */ return config && config->use_metadata && (config->preserve_perms || (config->preserve_times && !config->omit_dir_times) || config->preserve_owner || config->preserve_group || config->preserve_xattrs || config->preserve_acls); } void dir_time_list_init(DirTimeList* list) { if (!list) return; list->paths = NULL; list->entries = NULL; list->xattrs = NULL; list->count = 0; list->capacity = 0; list->bytes = 0; } void dir_time_list_free(DirTimeList* list) { if (!list) return; for (size_t i = 0; i < list->count; i++) { free(list->paths[i]); xattr_list_free(list->xattrs ? list->xattrs[i] : NULL); } free(list->paths); free(list->entries); free(list->xattrs); list->paths = NULL; list->entries = NULL; list->xattrs = NULL; list->count = 0; list->capacity = 0; list->bytes = 0; } bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetadata* metadata, const FileXattrList* xattrs) { if (!list || !wire_path || !metadata) return true; /* nothing to remember; never a hard error */ /* Cumulative, not per-frame: the sender may stream a tree across unbounded STATUS_DIR_TIMES frames, so bound the TOTAL retained here. Reject before touching the list, leaving it exactly as it was (the caller fails the transfer, which becomes a clean protocol error). */ size_t path_len = strlen(wire_path); /* Charge the whole per-entry cost (path copy + pointer slot + metadata struct + captured xattrs), not just the path, so the array growth is bounded by the same cumulative budget. */ size_t xattr_cost = 0; if (xattrs) { for (int i = 0; i < xattrs->count; i++) xattr_cost += strlen(xattrs->items[i].name) + xattrs->items[i].value_len + sizeof(FileXattr); } size_t entry_cost = path_len + sizeof(FileMetadata) + 2 * sizeof(char*) + xattr_cost; if (list->count >= MAX_DIR_TIME_ENTRIES || entry_cost > MAX_DIR_TIME_BYTES - list->bytes) return false; if (list->count == list->capacity) { size_t new_capacity = list->capacity == 0 ? 16 : list->capacity * 2; if (new_capacity < list->capacity) return false; /* Assign each grown array as soon as its realloc succeeds: the old block is already freed by then, so discarding the pointer would dangle. capacity is advanced only after ALL reallocs succeed, so a partial failure leaves capacity no larger than the smallest allocation -- never a mismatched list the next add could write past. */ char** grown_paths = realloc(list->paths, new_capacity * sizeof(char*)); if (!grown_paths) return false; list->paths = grown_paths; FileMetadata* grown_entries = realloc(list->entries, new_capacity * sizeof(FileMetadata)); if (!grown_entries) return false; list->entries = grown_entries; FileXattrList** grown_xattrs = realloc(list->xattrs, new_capacity * sizeof(FileXattrList*)); if (!grown_xattrs) return false; list->xattrs = grown_xattrs; list->capacity = new_capacity; } char* copy = str_dup(wire_path); if (!copy) return false; FileXattrList* xattr_copy = xattr_list_clone(xattrs); if (xattrs && !xattr_copy) { free(copy); return false; } list->paths[list->count] = copy; list->entries[list->count] = *metadata; list->xattrs[list->count] = xattr_copy; list->count++; list->bytes += entry_cost; return true; } void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory, const Config* config) { if (!list || !root_directory || !config) return; bool apply_times = config->preserve_times && !config->omit_dir_times; bool apply_mode = config->preserve_perms; bool apply_xattrs = config->use_xattrs; /* Ownership is applied through the active identity snapshot (which no-ops * unless an ownership request is active), and xattrs only when -X/-A was * negotiated. Times/mode keep their own per-attribute gates. */ bool have_any = apply_times || apply_mode || apply_xattrs || identity_active_enabled(); if (!have_any) return; for (size_t i = 0; i < list->count; i++) { char* dir_path = path_cat(root_directory, list->paths[i]); if (!dir_path) continue; char* leaf = NULL; /* The parent walk is fd-relative and O_NOFOLLOW, so a symlink planted in a parent component can never redirect the utimensat/chmod outside the root. */ int parent_fd = file_open_secure_parent(dir_path, &leaf, false); if (parent_fd < 0) { free(dir_path); continue; } /* A dir-time entry only records metadata: the directory is (deliberately) not created from it, so an empty source directory (or one pruned by -m/--prune-empty-dirs) may well not exist here. Skip absent paths QUIETLY rather than warning for every one, and apply the metadata only to a real directory that does exist. AT_SYMLINK_NOFOLLOW keeps a same-named symlink from being followed; a pre-existing regular file/symlink is not a directory, so it is left completely untouched. */ struct stat st; if (fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) != 0 || !S_ISDIR(st.st_mode)) { close(parent_fd); free(leaf); free(dir_path); continue; } /* One O_DIRECTORY|O_NOFOLLOW fd drives ownership/mode/xattr application so none of them can follow a same-named symlink planted after the fstatat. */ int dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); /* Ownership first: a chown clears setuid/setgid, so it must precede mode. */ if (dir_fd >= 0) identity_apply_ownership(dir_fd, (int32_t)list->entries[i].uid, (int32_t)list->entries[i].gid); if (apply_times) { struct timespec times[2] = { {.tv_sec = 0, .tv_nsec = UTIME_OMIT}, {.tv_sec = list->entries[i].mtime_sec, .tv_nsec = list->entries[i].mtime_nsec}}; if (config->preserve_atimes && list->entries[i].atime_valid) { times[0].tv_sec = list->entries[i].atime_sec; times[0].tv_nsec = list->entries[i].atime_nsec; } if (utimensat(parent_fd, leaf, times, AT_SYMLINK_NOFOLLOW) != 0) { char* escaped_path = output_escape(dir_path, log_get_8_bit_output()); log_message(LOG_LEVEL_WARNING, "Failed to set directory timestamps on %s: %s", escaped_path ? escaped_path : "", strerror(errno)); free(escaped_path); } } if (apply_mode) { mode_t dir_mode = list->entries[i].mode; bool mode_ready = true; if (config->chmod_spec && *config->chmod_spec && !chmod_apply(dir_mode, config->chmod_spec, &dir_mode)) { char* escaped_path = output_escape(dir_path, log_get_8_bit_output()); log_message(LOG_LEVEL_WARNING, "Failed to apply --chmod to directory %s", escaped_path ? escaped_path : ""); free(escaped_path); mode_ready = false; } if (mode_ready) { /* rsync -p copies the source directory mode exactly, including * group/other write and the setgid/sticky bits. Setuid/setgid/sticky * are super-user activities: when the connection forbade them * (SUPER_MODE_OFF / --no-super), strip them even under -p. */ mode_t safe_mode = dir_mode & (mode_t)(S_ISUID | S_ISGID | S_ISVTX | 0777); if (!privilege_super_mode_permitted(config->super_mode)) safe_mode &= ~(mode_t)(S_ISUID | S_ISGID | S_ISVTX); if (dir_fd < 0) { char* escaped_path = output_escape(dir_path, log_get_8_bit_output()); log_message(LOG_LEVEL_WARNING, "Failed to open directory %s to set its mode: %s", escaped_path ? escaped_path : "", strerror(errno)); free(escaped_path); } else if (fchmod(dir_fd, safe_mode) != 0) { char* escaped_path = output_escape(dir_path, log_get_8_bit_output()); log_message(LOG_LEVEL_WARNING, "Failed to set directory mode on %s: %s", escaped_path ? escaped_path : "", strerror(errno)); free(escaped_path); } } } /* xattrs/ACLs last: a mode change can rewrite the ACL mask, so the ACL xattrs must be (re)applied after fchmod. */ if (apply_xattrs && dir_fd >= 0 && list->xattrs) xattr_apply_fd(dir_fd, list->xattrs[i]); if (dir_fd >= 0) close(dir_fd); close(parent_fd); free(leaf); free(dir_path); } } /* Receive an explicit directory entry (--dirs): a STATUS_MKDIR frame carries the destination path and, when metadata is negotiated, the directory's metadata frame. The same path validation as a regular file applies (non-empty, relative-or-mirrored, no traversal), and the created File is routed through the regular store_file sink so single-threaded and -m receivers handle directories identically. The metadata is NOT applied here: the sink accumulates it into a DirTimeList that is applied only after the whole transfer (children would otherwise clobber the directory mtime). */ File* file_receive_directory(int file_descriptor, const Config* config) { char* path = receive_wire_str(file_descriptor); if (path == NULL) return NULL; if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) { char* escaped_path = output_escape(path, log_get_8_bit_output()); log_message(LOG_LEVEL_ERROR, "Invalid received directory path: %s", escaped_path ? escaped_path : ""); free(escaped_path); free(path); return NULL; } File* file = file_create(path); free(path); if (file == NULL) return NULL; file->is_dir = true; if (config && config->use_metadata) { int meta_ok = 1; file->metadata = metadata_receive(file_descriptor, &meta_ok); if (!meta_ok) { file_destroy(file); return NULL; } } /* Directory xattrs/ACLs (-X/-A) ride after the metadata when negotiated. */ if (config && !receive_file_xattrs(file, file_descriptor, config)) { file_destroy(file); return NULL; } return file; } /* Receive one directory-time entry from a STATUS_DIR_TIMES frame: the * destination-relative wire path and (when metadata is negotiated) the * directory's metadata frame. The created File is an is_dir, dir_time_only * entry routed through the regular store_file sink: the sink records its * metadata into the deferred DirTimeList but never creates the directory (the * scanner captures every traversed directory, including empty ones). Unlike a * STATUS_MKDIR entry, this one must not create anything. */ File* file_receive_dir_time(int file_descriptor, const Config* config) { char* path = receive_wire_str(file_descriptor); if (path == NULL) return NULL; if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) { char* escaped_path = output_escape(path, log_get_8_bit_output()); log_message(LOG_LEVEL_ERROR, "Invalid received directory-time path: %s", escaped_path ? escaped_path : ""); free(escaped_path); free(path); return NULL; } File* file = file_create(path); free(path); if (!file) return NULL; file->is_dir = true; file->dir_time_only = true; if (config && config->use_metadata) { int meta_ok = 1; file->metadata = metadata_receive(file_descriptor, &meta_ok); if (!meta_ok) { file_destroy(file); return NULL; } } /* Directory xattrs/ACLs (-X/-A) ride after the metadata, mirroring the sender's send_dir_times(). */ if (config && !receive_file_xattrs(file, file_descriptor, config)) { file_destroy(file); return NULL; } return file; } /* Receive a --hard-links/-H sibling frame (the leading STATUS_HARDLINK code has already been consumed): the destination path, the run-local link-group id, and the first (data-carrying) member's destination-relative wire path. The created File carries no payload; it is installed beneath the receive root as a hard link to (or, on link failure, a byte-identical copy of) the first member. All paths are validated like every other received path (non-empty, relative, no traversal). */ File* file_receive_hardlink(int file_descriptor) { char* path = receive_wire_str(file_descriptor); if (path == NULL) return NULL; if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) { char* escaped_path = output_escape(path, log_get_8_bit_output()); log_message(LOG_LEVEL_ERROR, "Invalid received hard-link path: %s", escaped_path ? escaped_path : ""); free(escaped_path); free(path); send_error_detail(file_descriptor, "invalid hard-link path"); return NULL; } int gid; if (!receive_int(file_descriptor, &gid) || gid <= 0) { free(path); return NULL; } char* target = receive_wire_str(file_descriptor); if (!target) { free(path); return NULL; } if (target[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(target))) { char* escaped = output_escape(target, log_get_8_bit_output()); log_message(LOG_LEVEL_ERROR, "Invalid hard-link target path: %s", escaped ? escaped : ""); free(escaped); free(target); free(path); send_error_detail(file_descriptor, "invalid hard-link target path"); return NULL; } File* file = file_create(path); free(path); if (file == NULL) { free(target); return NULL; } file->link_group = gid; file->link_first = false; file->hardlink_target = target; return file; } /* Receive a symlink entry (the leading STATUS_SYMLINK code has already been consumed): the destination path and the (sender-munged, if --munge-links) symlink target string, then metadata when negotiated. The created File is routed through the regular store_file sink, which creates the link beneath the receive root (unmungeing the target first). */ File* file_receive_symlink(int file_descriptor, const Config* config) { char* path = receive_wire_str(file_descriptor); if (path == NULL) return NULL; if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) { char* escaped_path = output_escape(path, log_get_8_bit_output()); log_message(LOG_LEVEL_ERROR, "Invalid received symlink path: %s", escaped_path ? escaped_path : ""); free(escaped_path); free(path); send_error_detail(file_descriptor, "invalid symlink path"); return NULL; } char* target = receive_wire_str(file_descriptor); if (!target) { free(path); return NULL; } if (target[0] == '\0') { char* escaped = output_escape(target, log_get_8_bit_output()); log_message(LOG_LEVEL_ERROR, "Invalid received symlink target: %s", escaped ? escaped : ""); free(escaped); free(target); free(path); send_error_detail(file_descriptor, "invalid symlink target"); return NULL; } File* file = file_create(path); free(path); if (!file) { free(target); return NULL; } if (config && config->use_metadata) { int meta_ok = 1; file->metadata = metadata_receive(file_descriptor, &meta_ok); if (!meta_ok) { file_destroy(file); free(target); return NULL; } } /* Symlink xattrs/ACLs (-X/-A) arrive in the same trailing block as the other entry kinds; the block is present iff use_xattrs (which itself implies use_metadata, so the metadata frame above is always consumed first). */ if (config && !receive_file_xattrs(file, file_descriptor, config)) { file_destroy(file); free(target); return NULL; } file->is_symlink = true; file->symlink_target = target; return file; } /* Receive a device/special node frame (--devices/--specials): the leading * STATUS_SPECIAL code has already been consumed. Payload: the destination path, * the metadata frame (whose mode's S_IFMT bits carry the node kind), and two * int32 rdev major/minor fields. The created File carries no payload and is * recreated by file_save_to_disk_full (mknod/mkfifo, privilege-gated and * confined). rdev is validated here (non-negative, range-checked) so a bogus * value cannot drive a dangerous node on the receiver. */ File* file_receive_special(int file_descriptor) { char* path = receive_wire_str(file_descriptor); if (path == NULL) return NULL; if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) { char* escaped_path = output_escape(path, log_get_8_bit_output()); log_message(LOG_LEVEL_ERROR, "Invalid received special path: %s", escaped_path ? escaped_path : ""); free(escaped_path); free(path); send_error_detail(file_descriptor, "invalid special path"); return NULL; } int meta_ok = 1; FileMetadata* metadata = metadata_receive(file_descriptor, &meta_ok); if (!meta_ok) { free(path); send_status(file_descriptor, STATUS_ERROR); return NULL; } int32_t major = 0; int32_t minor = 0; if (!receive_n_data(file_descriptor, &major, sizeof(major)) || !receive_n_data(file_descriptor, &minor, sizeof(minor))) { free(path); file_metadata_destroy(metadata); send_status(file_descriptor, STATUS_ERROR); return NULL; } /* A node kind must be present; without metadata mode there is no S_IFMT to recreate from. */ if (!metadata) { log_message(LOG_LEVEL_ERROR, "Special node sent without metadata (mode)"); free(path); send_error_detail(file_descriptor, "special node sent without metadata"); return NULL; } if (!file_special_rdev_valid(major, minor, metadata->mode)) { log_message(LOG_LEVEL_ERROR, "Invalid special rdev received (%d:%d)", (int)major, (int)minor); free(path); file_metadata_destroy(metadata); send_error_detail(file_descriptor, "invalid special device rdev"); return NULL; } File* file = file_create(path); free(path); if (file == NULL) { file_metadata_destroy(metadata); return NULL; } file->metadata = metadata; file->is_special = true; file->rdev_major = major; file->rdev_minor = minor; return file; }