devices: --devices/--specials/-D/--copy-devices/--write-devices
CI / lint (pull_request) Failing after 56s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
CI / lint (pull_request) Failing after 56s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
Recreate char/block nodes via mknodat (privilege-gated, EPERM->warn+skip) and FIFOs via mkfifoat; new STATUS_SPECIAL frame + validated rdev; sockets skipped; -copy-devices copies st_size; -write-devices O_NOFOLLOW+O_NONBLOCK warn+skip. preserve_specials/copy_devices/write_devices cross the wire. PROTOCOL_VERSION 2.12.0->2.13.0. Review fixes: -m source-removal keeps recreated specials, FIFO ENXIO skip, rdev bounds at chunk_deserialize, STATUS_ERROR on receive branch, scanner_prepare_special dedup.
This commit is contained in:
@@ -537,6 +537,11 @@ static const OptionEntry OPTION_TABLE[] = {
|
||||
{"--numeric-ids", NULL, OPT_FLAG, offsetof(Config, numeric_ids)},
|
||||
{"--atimes", "-U", OPT_FLAG, offsetof(Config, preserve_atimes)},
|
||||
{"--crtimes", "-N", OPT_FLAG, offsetof(Config, preserve_crtimes)},
|
||||
/* -D is handled separately (it implies both --devices and --specials). */
|
||||
{"--devices", NULL, OPT_FLAG, offsetof(Config, preserve_devices)},
|
||||
{"--specials", NULL, OPT_FLAG, offsetof(Config, preserve_specials)},
|
||||
{"--copy-devices", NULL, OPT_FLAG, offsetof(Config, copy_devices)},
|
||||
{"--write-devices", NULL, OPT_FLAG, offsetof(Config, write_devices)},
|
||||
{"--omit-dir-times", "-O", OPT_FLAG, offsetof(Config, omit_dir_times)},
|
||||
{"--omit-link-times", "-J", OPT_FLAG, offsetof(Config, omit_link_times)},
|
||||
{"--open-noatime", NULL, OPT_FLAG, offsetof(Config, open_noatime)},
|
||||
@@ -831,6 +836,12 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
} else if (opt_is(argv[i], "-V", "--version")) {
|
||||
printf("fastsync version %s\n", PROTOCOL_VERSION);
|
||||
return 1;
|
||||
} else if (opt_is(argv[i], "-D", NULL)) {
|
||||
/* rsync -D == --devices --specials. -D is otherwise unassigned in
|
||||
FastSync (verified: no collision), so it is free to imply both. */
|
||||
config->preserve_devices = true;
|
||||
config->preserve_specials = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled preservation of device and special files (-D)");
|
||||
} else if (opt_is(argv[i], "-a", "--archive")) {
|
||||
config->use_compression =
|
||||
!config->compress_choice || strcmp(config->compress_choice, "zstd") == 0;
|
||||
@@ -1200,6 +1211,15 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
config->files_from_set = set;
|
||||
}
|
||||
|
||||
/* Device/special preservation recreates a node from its metadata mode (whose
|
||||
S_IFMT bits carry the node kind), so --devices/--specials/-D imply metadata
|
||||
transmission. --copy-devices/--write-devices treat the entry as data but a
|
||||
mtime/mode-preserving transfer still benefits from metadata, so all four
|
||||
imply it (FastSync's broad -M bundle; ownership stays opt-in). */
|
||||
if (config->preserve_devices || config->preserve_specials || config->copy_devices ||
|
||||
config->write_devices)
|
||||
config->use_metadata = true;
|
||||
|
||||
/* The "unchanged" decision for --compare-dest/--copy-dest/--link-dest must
|
||||
* be made on the receiver against the basis directories, which requires the
|
||||
* per-file STATUS_CHECK handshake: basis-dir options therefore imply
|
||||
|
||||
@@ -104,6 +104,9 @@ static bool prepare_scanner(const Config* config, int num_threads, PreparedScann
|
||||
options->copy_unsafe_links = config->copy_unsafe_links;
|
||||
options->checksum = config->checksum;
|
||||
options->one_file_system = config->one_file_system;
|
||||
options->preserve_devices = config->preserve_devices;
|
||||
options->preserve_specials = config->preserve_specials;
|
||||
options->copy_devices = config->copy_devices;
|
||||
options->file_list = (const FileListSet*)config->files_from_set;
|
||||
options->base_filters = out->base_filters;
|
||||
options->per_dir_filters = config->per_dir_filter;
|
||||
@@ -1248,6 +1251,14 @@ static int send_chunk_with_removal(Client* client, Chunk* chunk, Config* config,
|
||||
change_emit_file_sent(config, f);
|
||||
continue;
|
||||
}
|
||||
/* --devices/--specials: a device/special node is recreated on the receiver,
|
||||
not transferred as content. Send the dedicated STATUS_SPECIAL frame. */
|
||||
if (f->is_special) {
|
||||
if (!file_send_special(f, client->file_descriptor, config->use_metadata))
|
||||
return -1;
|
||||
change_emit_file_sent(config, f);
|
||||
continue;
|
||||
}
|
||||
bool stream = f->data->data == NULL && f->data->size > 0;
|
||||
bool use_sendfile =
|
||||
(config->use_sendfile && !config->use_compression) || (stream && !config->use_compression);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <threads.h>
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
@@ -193,6 +194,34 @@ static void scanner_assign_hardlink(DirectoryScanner* scanner, HardLinkTable* ta
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 4 special/devices: detect a device (char/block), FIFO or socket entry
|
||||
and, when the matching --devices/--specials flag asks it be preserved,
|
||||
convert the File into a node to recreate (is_special, empty payload) with its
|
||||
device rdev captured from the source stat. When the entry is not preserved
|
||||
(or --copy-devices instead copies its content as an ordinary regular file)
|
||||
the File is left as a normal data file. Returns true when converted. */
|
||||
static bool scanner_prepare_special(bool preserve_devices, bool preserve_specials, File* file,
|
||||
const struct stat* stats) {
|
||||
if (!file || !stats)
|
||||
return false;
|
||||
bool is_device = S_ISCHR(stats->st_mode) || S_ISBLK(stats->st_mode);
|
||||
bool is_fifo = S_ISFIFO(stats->st_mode);
|
||||
bool is_socket = S_ISSOCK(stats->st_mode);
|
||||
if (!is_device && !is_fifo && !is_socket)
|
||||
return false;
|
||||
bool preserve = is_device ? preserve_devices : preserve_specials;
|
||||
if (!preserve)
|
||||
return false;
|
||||
file->is_special = true;
|
||||
file->data->size = 0;
|
||||
file->data->data = NULL;
|
||||
if (is_device) {
|
||||
file->rdev_major = (int32_t)major(stats->st_rdev);
|
||||
file->rdev_minor = (int32_t)minor(stats->st_rdev);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Append `rel` to the caller's exclusion sink, taking `mtx` when shared across
|
||||
parallel worker threads. Returns false on allocation failure (list left
|
||||
unchanged). */
|
||||
@@ -365,6 +394,9 @@ DirectoryScanner* directory_scanner_create_with_options(const char* root_directo
|
||||
scanner->copy_unsafe_links = options->copy_unsafe_links;
|
||||
scanner->checksum = options->checksum;
|
||||
scanner->one_file_system = options->one_file_system;
|
||||
scanner->preserve_devices = options->preserve_devices;
|
||||
scanner->preserve_specials = options->preserve_specials;
|
||||
scanner->copy_devices = options->copy_devices;
|
||||
scanner->failed = false;
|
||||
scanner->root_path = str_dup(root_directory);
|
||||
if (!scanner->root_path) {
|
||||
@@ -925,6 +957,9 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
||||
file->send_path = rel_copy;
|
||||
rel_copy = NULL;
|
||||
}
|
||||
/* --devices/--specials: a device/FIFO/socket entry marked for preservation
|
||||
becomes a node to recreate (is_special, no data, rdev captured). */
|
||||
scanner_prepare_special(scanner->preserve_devices, scanner->preserve_specials, file, &stats);
|
||||
if (scanner->hardlinks && S_ISREG(stats.st_mode))
|
||||
scanner_assign_hardlink(scanner, scanner->hardlinks, file, &stats);
|
||||
if (scanner->use_metadata)
|
||||
@@ -1243,6 +1278,7 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
|
||||
file->send_path = rel;
|
||||
rel = NULL;
|
||||
}
|
||||
scanner_prepare_special(options->preserve_devices, options->preserve_specials, file, &st);
|
||||
if (options->hardlinks && S_ISREG(st.st_mode)) {
|
||||
int gid;
|
||||
bool is_first;
|
||||
|
||||
@@ -34,6 +34,12 @@ typedef struct {
|
||||
bool copy_unsafe_links;
|
||||
bool checksum;
|
||||
bool one_file_system;
|
||||
/* Phase 4 special/devices: whether device nodes (--devices) and special files
|
||||
* (--specials) are preserved via recreation, and whether --copy-devices
|
||||
* copies a device's content as an ordinary regular file. */
|
||||
bool preserve_devices;
|
||||
bool preserve_specials;
|
||||
bool copy_devices;
|
||||
/* Phase 2 (files-from / filter layer). All pointers are shared read-only
|
||||
* across scanner instances and worker threads; ownership stays with the
|
||||
* caller (client_send). */
|
||||
@@ -104,6 +110,10 @@ typedef struct {
|
||||
bool one_file_system;
|
||||
dev_t root_dev;
|
||||
bool failed;
|
||||
/* Phase 4 special/devices (see ScannerOptions). */
|
||||
bool preserve_devices;
|
||||
bool preserve_specials;
|
||||
bool copy_devices;
|
||||
/* Phase 2 (files-from / filter layer). */
|
||||
char* root_path; /* transfer root (fs path) for rel computation */
|
||||
char* current_rel; /* rel path of the open directory ("" == root) */
|
||||
|
||||
@@ -182,6 +182,15 @@ void print_usage(void) {
|
||||
printf(" --copy-unsafe-links Only transform unsafe symlinks into referent files\n");
|
||||
printf(" -H, --hard-links Preserve hard-link relationships across the transfer\n");
|
||||
printf(" -S, --sparse Handle sparse files efficiently\n");
|
||||
printf(
|
||||
" -D Preserve device and special files (implies --devices --specials)\n");
|
||||
printf(
|
||||
" --devices Recreate device nodes on the destination (privileged; skipped when\n");
|
||||
printf(" the receiver lacks CAP_MKNOD)\n");
|
||||
printf(" --specials Recreate special files (FIFOs) on the destination (sockets "
|
||||
"skipped)\n");
|
||||
printf(" --copy-devices Copy a source device's content as a regular file instead\n");
|
||||
printf(" --write-devices Write received data into an existing destination device node\n");
|
||||
printf(" --inplace Update files in-place (no temp+rename)\n");
|
||||
printf(
|
||||
" --preallocate Allocate destination file space up front (fail-fast on full disk)\n");
|
||||
|
||||
@@ -154,7 +154,8 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
||||
DeleteManifest* deferred_manifest = NULL;
|
||||
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK ||
|
||||
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH ||
|
||||
status == STATUS_MKDIR || status == STATUS_MANIFEST || status == STATUS_HARDLINK) {
|
||||
status == STATUS_MKDIR || status == STATUS_MANIFEST || status == STATUS_HARDLINK ||
|
||||
status == STATUS_SPECIAL) {
|
||||
if (status == STATUS_KEEPALIVE) {
|
||||
if (!send_status(file_descriptor, STATUS_KEEPALIVE))
|
||||
goto fail;
|
||||
@@ -185,6 +186,10 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
||||
File* file = file_receive_hardlink(file_descriptor);
|
||||
if (!file || !sink->store_file(file, sink->context))
|
||||
goto receive_error;
|
||||
} else if (status == STATUS_SPECIAL) {
|
||||
File* file = file_receive_special(file_descriptor);
|
||||
if (!file || !sink->store_file(file, sink->context))
|
||||
goto receive_error;
|
||||
} else if (status == STATUS_MANIFEST) {
|
||||
DeleteManifest* manifest = receive_manifest_entries(file_descriptor);
|
||||
if (!manifest)
|
||||
@@ -309,7 +314,8 @@ static bool receiver_save_file(File* file, void* context_pointer) {
|
||||
result = file_save_to_disk_full(context->config->receive_root_directory, file, context->config);
|
||||
}
|
||||
if (result != FILE_SAVE_ERROR && context->config->remove_source_files && !file->is_dir &&
|
||||
!file->skip && !receiver_outcomes_append(&context->outcomes, (unsigned char)result)) {
|
||||
!file->is_special && !file->skip &&
|
||||
!receiver_outcomes_append(&context->outcomes, (unsigned char)result)) {
|
||||
file_destroy(file);
|
||||
return false;
|
||||
}
|
||||
|
||||
+48
-3
@@ -74,10 +74,17 @@ static unsigned long long per_file_serialize_size(File* file, bool use_metadata)
|
||||
if (metadata_size > ULLONG_MAX - size)
|
||||
return 0;
|
||||
size += metadata_size;
|
||||
/* Entry type marker: 0 = regular file, 1 = explicit directory entry. */
|
||||
/* Entry type marker: 0 = regular file, 1 = explicit directory entry,
|
||||
* 2 = special/device node (recreated by the receiver). */
|
||||
if (sizeof(int) > ULLONG_MAX - size)
|
||||
return 0;
|
||||
size += sizeof(int);
|
||||
/* A special node also carries its rdev major/minor. */
|
||||
if (file->is_special) {
|
||||
if (2 * sizeof(int32_t) > ULLONG_MAX - size)
|
||||
return 0;
|
||||
size += 2 * sizeof(int32_t);
|
||||
}
|
||||
if (sizeof(size_t) > ULLONG_MAX - size)
|
||||
return 0;
|
||||
size += sizeof(size_t);
|
||||
@@ -116,10 +123,19 @@ Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
|
||||
memcpy(data_pointer, wire_path, path_len);
|
||||
data_pointer += path_len;
|
||||
|
||||
int entry_type = file->is_dir ? 1 : 0;
|
||||
int entry_type = file->is_dir ? 1 : (file->is_special ? 2 : 0);
|
||||
memcpy(data_pointer, &entry_type, sizeof(int));
|
||||
data_pointer += sizeof(int);
|
||||
|
||||
if (file->is_special) {
|
||||
int32_t special_major = file->rdev_major;
|
||||
int32_t special_minor = file->rdev_minor;
|
||||
memcpy(data_pointer, &special_major, sizeof(special_major));
|
||||
data_pointer += sizeof(special_major);
|
||||
memcpy(data_pointer, &special_minor, sizeof(special_minor));
|
||||
data_pointer += sizeof(special_minor);
|
||||
}
|
||||
|
||||
if (use_metadata)
|
||||
metadata_to_buf(&data_pointer, file->metadata);
|
||||
|
||||
@@ -206,16 +222,45 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
}
|
||||
int entry_type;
|
||||
memcpy(&entry_type, data_pointer, sizeof(int));
|
||||
if (entry_type != 0 && entry_type != 1) {
|
||||
if (entry_type != 0 && entry_type != 1 && entry_type != 2) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: bad entry type");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
file->is_dir = entry_type == 1;
|
||||
file->is_special = entry_type == 2;
|
||||
data_pointer += sizeof(int);
|
||||
remaining_size -= sizeof(int);
|
||||
|
||||
if (file->is_special) {
|
||||
if (remaining_size < 2 * (int32_t)sizeof(int32_t)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for special rdev");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
int32_t special_major, special_minor;
|
||||
memcpy(&special_major, data_pointer, sizeof(special_major));
|
||||
data_pointer += sizeof(special_major);
|
||||
memcpy(&special_minor, data_pointer, sizeof(special_minor));
|
||||
data_pointer += sizeof(special_minor);
|
||||
remaining_size -= 2 * sizeof(int32_t);
|
||||
/* Reject an out-of-range/negative rdev here as a malformed chunk (the
|
||||
same 0xffff / 0x00ffffff bounds file_special_rdev_valid uses), so a
|
||||
bogus large-but-positive rdev is refused cleanly instead of being
|
||||
deferred to the creation site where it would abort after the frame. */
|
||||
if (special_major < 0 || special_minor < 0 || special_major > 0xffff ||
|
||||
special_minor > 0x00ffffff) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: out-of-range special rdev");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
file->rdev_major = special_major;
|
||||
file->rdev_minor = special_minor;
|
||||
}
|
||||
|
||||
if (use_metadata) {
|
||||
if (remaining_size < sizeof(int)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for metadata");
|
||||
|
||||
+22
-12
@@ -76,6 +76,9 @@ static void config_set_defaults(Config* config) {
|
||||
config->preserve_xattrs = false;
|
||||
config->preserve_devices = false;
|
||||
config->preserve_sparse = false;
|
||||
config->preserve_specials = false;
|
||||
config->copy_devices = false;
|
||||
config->write_devices = false;
|
||||
config->itemize_changes = false;
|
||||
config->out_format = NULL;
|
||||
config->log_file_format = NULL;
|
||||
@@ -180,16 +183,18 @@ static bool validate_received_config(const Config* config) {
|
||||
valid_wire_bool(config->safe_links) && valid_wire_bool(config->copy_unsafe_links) &&
|
||||
valid_wire_bool(config->preserve_hard_links) && valid_wire_bool(config->preserve_acls) &&
|
||||
valid_wire_bool(config->preserve_xattrs) && valid_wire_bool(config->preserve_devices) &&
|
||||
valid_wire_bool(config->preserve_sparse) && valid_wire_bool(config->ignore_existing) &&
|
||||
valid_wire_bool(config->existing) && valid_wire_bool(config->update) &&
|
||||
valid_wire_bool(config->inplace) && valid_wire_bool(config->append) &&
|
||||
valid_wire_bool(config->use_fsync) && valid_wire_bool(config->append_verify) &&
|
||||
valid_wire_bool(config->delete_excluded) && valid_wire_bool(config->force_delete) &&
|
||||
valid_wire_bool(config->delete_missing_args) && valid_wire_bool(config->delete_after) &&
|
||||
valid_wire_bool(config->preallocate) && valid_wire_bool(config->delete_delay) &&
|
||||
valid_wire_bool(config->delete_during) && valid_wire_bool(config->relative) &&
|
||||
valid_wire_bool(config->prune_empty_dirs) && valid_wire_bool(config->delay_updates) &&
|
||||
valid_wire_bool(config->mkpath) && !(config->delay_updates && config->inplace) &&
|
||||
valid_wire_bool(config->preserve_sparse) && valid_wire_bool(config->preserve_specials) &&
|
||||
valid_wire_bool(config->copy_devices) && valid_wire_bool(config->write_devices) &&
|
||||
valid_wire_bool(config->ignore_existing) && valid_wire_bool(config->existing) &&
|
||||
valid_wire_bool(config->update) && valid_wire_bool(config->inplace) &&
|
||||
valid_wire_bool(config->append) && valid_wire_bool(config->use_fsync) &&
|
||||
valid_wire_bool(config->append_verify) && valid_wire_bool(config->delete_excluded) &&
|
||||
valid_wire_bool(config->force_delete) && valid_wire_bool(config->delete_missing_args) &&
|
||||
valid_wire_bool(config->delete_after) && valid_wire_bool(config->preallocate) &&
|
||||
valid_wire_bool(config->delete_delay) && valid_wire_bool(config->delete_during) &&
|
||||
valid_wire_bool(config->relative) && valid_wire_bool(config->prune_empty_dirs) &&
|
||||
valid_wire_bool(config->delay_updates) && valid_wire_bool(config->mkpath) &&
|
||||
!(config->delay_updates && config->inplace) &&
|
||||
!(config->delay_updates && delay_updates_staging_name_conflict(config->backup_dir)) &&
|
||||
valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) &&
|
||||
valid_wire_bool(config->checksum) && valid_wire_bool(config->eight_bit_output) &&
|
||||
@@ -444,12 +449,16 @@ static bool send_delta_fields(int fd, const Config* c) {
|
||||
}
|
||||
|
||||
static bool send_file_options(int fd, const Config* c) {
|
||||
/* Device/special preservation flags cross the wire so the receiver knows a
|
||||
* special/device entry must be recreated. Trailing fields; protocol 2.13.0. */
|
||||
return send_int(fd, c->backup) && send_str(fd, c->backup_dir ? c->backup_dir : "") &&
|
||||
send_int(fd, c->remove_source_files) && send_int(fd, c->follow_symlinks) &&
|
||||
send_int(fd, c->copy_links) && send_int(fd, c->safe_links) &&
|
||||
send_int(fd, c->copy_unsafe_links) && send_int(fd, c->preserve_hard_links) &&
|
||||
send_int(fd, c->preserve_acls) && send_int(fd, c->preserve_xattrs) &&
|
||||
send_int(fd, c->preserve_devices) && send_int(fd, c->preserve_sparse);
|
||||
send_int(fd, c->preserve_devices) && send_int(fd, c->preserve_sparse) &&
|
||||
send_int(fd, c->preserve_specials) && send_int(fd, c->copy_devices) &&
|
||||
send_int(fd, c->write_devices);
|
||||
}
|
||||
|
||||
static bool send_selection_options(int fd, const Config* c) {
|
||||
@@ -569,7 +578,8 @@ static bool receive_file_options(int fd, Config* c) {
|
||||
return false;
|
||||
bool* flags[] = {&c->follow_symlinks, &c->copy_links, &c->safe_links,
|
||||
&c->copy_unsafe_links, &c->preserve_hard_links, &c->preserve_acls,
|
||||
&c->preserve_xattrs, &c->preserve_devices, &c->preserve_sparse};
|
||||
&c->preserve_xattrs, &c->preserve_devices, &c->preserve_sparse,
|
||||
&c->preserve_specials, &c->copy_devices, &c->write_devices};
|
||||
for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) {
|
||||
if (!receive_wire_bool(fd, flags[i]))
|
||||
return false;
|
||||
|
||||
+17
-1
@@ -115,6 +115,22 @@ typedef struct Config {
|
||||
bool preserve_xattrs;
|
||||
bool preserve_devices;
|
||||
bool preserve_sparse;
|
||||
/* Phase 4 special/devices: preserve special files (FIFOs, sockets) and device
|
||||
* nodes on the destination by recreating them (mknod/mkfifo) instead of
|
||||
* transferring content. preserve_specials mirrors rsync --specials (the
|
||||
* special-file half of -D); preserve_devices mirrors --devices (the device
|
||||
* half of -D); both CROSS the wire so the receiver knows a special/device
|
||||
* entry must be recreated rather than written as a regular file. */
|
||||
bool preserve_specials;
|
||||
/* --copy-devices: copy the CONTENT of a source device as an ordinary regular
|
||||
* file on the destination (rsync's non-privileged safe mode), instead of
|
||||
* recreating the device node. CROSSES the wire (receiver treats the entry as
|
||||
* a regular file, which is the default, so this is belt-and-braces). */
|
||||
bool copy_devices;
|
||||
/* --write-devices: write the received data directly INTO an existing device
|
||||
* node on the destination instead of creating a regular file. Dangeroud;
|
||||
* see RSYNC_COMPAT.md for the tight gating. CROSSES the wire. */
|
||||
bool write_devices;
|
||||
|
||||
// Issue #122: Output/logging options
|
||||
bool itemize_changes;
|
||||
@@ -312,7 +328,7 @@ typedef struct Config {
|
||||
bool open_noatime;
|
||||
} Config;
|
||||
|
||||
#define PROTOCOL_VERSION "2.12.0"
|
||||
#define PROTOCOL_VERSION "2.13.0"
|
||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
||||
#define MAX_BASIS_DIRS 64
|
||||
|
||||
@@ -114,6 +114,9 @@ File* file_create(const char* path) {
|
||||
file->link_group = 0;
|
||||
file->link_first = false;
|
||||
file->hardlink_target = NULL;
|
||||
file->is_special = false;
|
||||
file->rdev_major = 0;
|
||||
file->rdev_minor = 0;
|
||||
return file;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "array_list.h"
|
||||
@@ -288,6 +289,231 @@ static FileSaveResult file_save_hardlink_sibling(const char* root_directory, con
|
||||
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
/* Validate a transmitted special rdev against the node kind implied by `mode`'s
|
||||
* S_IFMT bits. Char/block devices require a legal major/minor pair (non-negative,
|
||||
* range-checked); a non-device special (FIFO/socket) must carry an empty rdev.
|
||||
* Used identically on the wire path and at the secure recreation site so a
|
||||
* malicious/bogus rdev can never drive a dangerous node. */
|
||||
bool file_special_rdev_valid(int32_t major, int32_t minor, mode_t mode) {
|
||||
bool is_device = S_ISCHR(mode) || S_ISBLK(mode);
|
||||
if (is_device)
|
||||
return major >= 0 && minor >= 0 && major <= 0xffff && minor <= 0x00ffffff;
|
||||
/* A non-device entry must actually be a special (FIFO/socket) and carry no
|
||||
rdev; a regular/dir mode is never a valid special node. */
|
||||
return (S_ISFIFO(mode) || S_ISSOCK(mode)) && major == 0 && minor == 0;
|
||||
}
|
||||
|
||||
/* ---- Device/special node RECREATION (--devices/--specials), receiver side ----
|
||||
*
|
||||
* Privilege gating: making a real device node requires CAP_MKNOD (root); making
|
||||
* a FIFO works unprivileged (mkfifo). When the receiver lacks the capability,
|
||||
* mknodat() fails with EPERM and the entry is SKIPPED with a warning -- the
|
||||
* whole transfer must NOT abort just because the environment cannot make the
|
||||
* node. CI runs non-root, so device creation is expected to skip there and
|
||||
* only a FIFO is honestly assertable unprivileged.
|
||||
*
|
||||
* Confinement: the parent directory is opened fd-relative below the receive
|
||||
* root (file_open_secure_parent: O_NOFOLLOW, no "..", root-checked) and the
|
||||
* node is created with mknodat()/mkfifoat(), so it can never be placed outside
|
||||
* the confined root and never follows a symlink.
|
||||
*
|
||||
* rdev validation: a malicious/bogus rdev (negative, out-of-range) is rejected
|
||||
* here as well as on the wire (file_receive_special / chunk_deserialize), and a
|
||||
* non-device entry must carry an empty rdev.
|
||||
*/
|
||||
static FileSaveResult file_save_special_to_disk(const char* root_directory, const File* file,
|
||||
const Config* config) {
|
||||
if (!root_directory || !file || !file->path || file->path[0] == '\0' ||
|
||||
has_path_traversal(file->path) || !file->metadata)
|
||||
return FILE_SAVE_ERROR;
|
||||
|
||||
mode_t mode = file->metadata->mode;
|
||||
bool is_char = S_ISCHR(mode);
|
||||
bool is_blk = S_ISBLK(mode);
|
||||
bool is_fifo = S_ISFIFO(mode);
|
||||
bool is_sock = S_ISSOCK(mode);
|
||||
if (!is_char && !is_blk && !is_fifo && !is_sock) {
|
||||
log_message(LOG_LEVEL_ERROR, "Special node has no device/FIFO/socket mode");
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
if (is_sock) {
|
||||
/* No standard filesystem call recreates a socket; best-effort unsupported. */
|
||||
log_message(LOG_LEVEL_WARNING, "socket not recreated: %s (unsupported; skipped)", file->path);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
if (is_char || is_blk) {
|
||||
if (!config || !config->preserve_devices)
|
||||
return FILE_SAVE_SKIPPED;
|
||||
} else if (is_fifo) {
|
||||
if (!config || !config->preserve_specials)
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
/* Defense-in-depth rdev/type validation (also done on the wire path). */
|
||||
if (!file_special_rdev_valid(file->rdev_major, file->rdev_minor, mode)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Rejected out-of-range device rdev %d:%d", file->rdev_major,
|
||||
file->rdev_minor);
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
char* destination = path_cat(root_directory, file->path);
|
||||
if (!destination)
|
||||
return FILE_SAVE_ERROR;
|
||||
char* leaf = NULL;
|
||||
int parent_fd = file_open_secure_parent(destination, &leaf, true);
|
||||
if (parent_fd < 0) {
|
||||
free(destination);
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
/* --existing / --ignore-existing / --update decide against the node that
|
||||
would be replaced, mirroring the regular-file path. */
|
||||
if (config->existing && !file_path_exists_secure(destination)) {
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
free(destination);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
if (config->ignore_existing && file_path_exists_secure(destination)) {
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
free(destination);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
if (config->update && file_destination_is_newer_secure(destination, file->metadata)) {
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
free(destination);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
|
||||
dev_t rdev = 0;
|
||||
mode_t create_mode;
|
||||
if (is_char) {
|
||||
create_mode = S_IFCHR;
|
||||
rdev = makedev((unsigned)file->rdev_major, (unsigned)file->rdev_minor);
|
||||
} else if (is_blk) {
|
||||
create_mode = S_IFBLK;
|
||||
rdev = makedev((unsigned)file->rdev_major, (unsigned)file->rdev_minor);
|
||||
} else {
|
||||
create_mode = S_IFIFO;
|
||||
}
|
||||
mode_t perms = mode & 0777;
|
||||
|
||||
int rc = is_fifo ? mkfifoat(parent_fd, leaf, perms)
|
||||
: mknodat(parent_fd, leaf, create_mode | perms, rdev);
|
||||
if (rc != 0) {
|
||||
if (errno == EEXIST) {
|
||||
/* An entry already exists: only skip when it already is a matching node;
|
||||
never replace an existing directory or unrelated entry with the node. */
|
||||
struct stat st;
|
||||
if (fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) == 0 &&
|
||||
((is_char && S_ISCHR(st.st_mode)) || (is_blk && S_ISBLK(st.st_mode)) ||
|
||||
(is_fifo && S_ISFIFO(st.st_mode)))) {
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
free(destination);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
log_message(LOG_LEVEL_WARNING, "refusing to replace existing entry with %s: %s (skipped)",
|
||||
is_fifo ? "FIFO" : "device", file->path);
|
||||
} else if (errno == EPERM || errno == EACCES) {
|
||||
/* Missing CAP_MKNOD / parent write permission: the environment cannot
|
||||
create the node, so skip instead of failing the whole run. */
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"skipping %s: cannot create %s node (%s)\n"
|
||||
" --devices/--specials node creation needs privilege (CAP_MKNOD)",
|
||||
file->path, is_fifo ? "FIFO" : "device", strerror(errno));
|
||||
} else {
|
||||
log_message(LOG_LEVEL_WARNING, "failed to create %s %s: %s (skipped)",
|
||||
is_fifo ? "FIFO" : "device", file->path, strerror(errno));
|
||||
}
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
free(destination);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
|
||||
/* Apply mtime on the fresh node (utimensat, no-follow). Ownership is not
|
||||
applied -- identity fchown needs an fd and would require opening the node. */
|
||||
struct timespec times[2] = {
|
||||
{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
|
||||
{.tv_sec = file->metadata->mtime_sec, .tv_nsec = file->metadata->mtime_nsec}};
|
||||
utimensat(parent_fd, leaf, times, AT_SYMLINK_NOFOLLOW);
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
free(destination);
|
||||
return FILE_SAVE_WRITTEN;
|
||||
}
|
||||
|
||||
/* --write-devices (receiver): write the received data directly into an EXISTING
|
||||
* device node on the destination instead of creating a regular file. The node
|
||||
* must already exist and be a char/block device (the device itself is opened and
|
||||
* followed); it is confined to the receive root via file_open_secure_parent.
|
||||
* Dangerous by nature, so deliberately restricted: a missing/non-device
|
||||
* destination, or a write failure, is SKIPPED with a warning rather than
|
||||
* allowed. On environments without device access the run still succeeds (the
|
||||
* entry is skipped), never aborts. */
|
||||
static FileSaveResult file_save_write_device(const char* root_directory, const File* file) {
|
||||
if (!root_directory || !file || !file->path || file->path[0] == '\0' ||
|
||||
has_path_traversal(file->path))
|
||||
return FILE_SAVE_ERROR;
|
||||
if (!file->data)
|
||||
return FILE_SAVE_ERROR;
|
||||
char* destination = path_cat(root_directory, file->path);
|
||||
if (!destination)
|
||||
return FILE_SAVE_ERROR;
|
||||
char* leaf = NULL;
|
||||
int parent_fd = file_open_secure_parent(destination, &leaf, false);
|
||||
if (parent_fd < 0) {
|
||||
free(destination);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
/* O_NONBLOCK: a pre-existing FIFO at the target would otherwise block the
|
||||
receive thread forever on open(2). With it the open only succeeds for a
|
||||
readerless FIFO with O_RDWR (which the device fstat gate rejects anyway)
|
||||
or fails with ENXIO/EAGAIN, both treated as a normal skip below. */
|
||||
int fd = openat(parent_fd, leaf, O_WRONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
|
||||
int saved_errno = errno;
|
||||
free(leaf);
|
||||
close(parent_fd);
|
||||
if (fd < 0) {
|
||||
free(destination);
|
||||
if (saved_errno == ENXIO || saved_errno == EAGAIN) {
|
||||
/* A FIFO with no reader / an unreadable special: skip like every other
|
||||
unusable write-devices target instead of blocking or failing. */
|
||||
log_message(LOG_LEVEL_WARNING, "write-devices: %s not writable (%s); skipped", file->path,
|
||||
strerror(saved_errno));
|
||||
} else {
|
||||
log_message(LOG_LEVEL_WARNING, "write-devices: cannot open %s (%s); skipped", file->path,
|
||||
strerror(saved_errno));
|
||||
}
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) != 0 || !(S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))) {
|
||||
close(fd);
|
||||
free(destination);
|
||||
log_message(LOG_LEVEL_WARNING, "write-devices: %s is not a device node; skipped", file->path);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
bool ok = true;
|
||||
if (file->data->size > 0) {
|
||||
size_t total = (size_t)file->data->size;
|
||||
size_t written = 0;
|
||||
while (written < total) {
|
||||
ssize_t n = write(fd, (char*)file->data->data + written, total - written);
|
||||
if (n <= 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
written += (size_t)n;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
free(destination);
|
||||
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_SKIPPED;
|
||||
}
|
||||
|
||||
FileSaveResult file_save_to_disk_full(const char* root_directory, const File* file,
|
||||
const Config* config) {
|
||||
/* Backups are incompatible with ignore-existing: moving the entry first
|
||||
@@ -314,6 +540,14 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
/* Device/special node (--devices/--specials): recreate the node instead of
|
||||
writing content (privilege-gated, confined, rdev-validated). */
|
||||
if (file->is_special)
|
||||
return file_save_special_to_disk(root_directory, file, config);
|
||||
/* --write-devices: write straight into an existing device node. */
|
||||
if (config && config->write_devices)
|
||||
return file_save_write_device(root_directory, file);
|
||||
|
||||
/* Explicit directory entries (--dirs) carry an empty payload; the entry is
|
||||
created as a directory under the receive root, applying the same secure
|
||||
mkdir-parent semantics as regular writes. Directories are created
|
||||
@@ -1868,6 +2102,70 @@ File* file_receive_hardlink(int file_descriptor) {
|
||||
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_str(file_descriptor);
|
||||
if (path == NULL)
|
||||
return NULL;
|
||||
if (path[0] == '\0' || 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 : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
free(path);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
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_status(file_descriptor, STATUS_ERROR);
|
||||
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_status(file_descriptor, STATUS_ERROR);
|
||||
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;
|
||||
}
|
||||
|
||||
/* Read a delete-manifest frame (the STATUS_MANIFEST leading code has already
|
||||
been consumed): a keep-set entry count followed by that many
|
||||
destination-relative paths, then a protected-prefix count followed by that
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
File* file_receive(const Config* config, int file_descriptor);
|
||||
File* file_receive_directory(int file_descriptor);
|
||||
File* file_receive_hardlink(int file_descriptor);
|
||||
File* file_receive_special(int file_descriptor);
|
||||
bool file_special_rdev_valid(int32_t major, int32_t minor, mode_t mode);
|
||||
File* receive_incremental_check(int fd, const Config* config, bool* skipped);
|
||||
|
||||
/* A received delete-manifest frame: the keep-set (`keeps`, destination-relative
|
||||
|
||||
@@ -17,6 +17,25 @@
|
||||
#include "metadata.h"
|
||||
#include "protocol.h"
|
||||
|
||||
/* Transmit a device/special node (--devices / --specials) as a STATUS_SPECIAL
|
||||
* frame: the destination path, the metadata frame (whose mode's S_IFMT bits
|
||||
* carry the node kind) and the device rdev major/minor. The receiver validates
|
||||
* the kind and rdev and recreates the node (privilege-gating the mknod). */
|
||||
bool file_send_special(File* file, int file_descriptor, bool use_metadata) {
|
||||
if (!file || !file_wire_path(file))
|
||||
return false;
|
||||
if (!send_status(file_descriptor, STATUS_SPECIAL))
|
||||
return false;
|
||||
if (!send_str(file_descriptor, file_wire_path(file)))
|
||||
return false;
|
||||
if (use_metadata && !metadata_send(file_descriptor, file->metadata))
|
||||
return false;
|
||||
int32_t major = file->rdev_major;
|
||||
int32_t minor = file->rdev_minor;
|
||||
return send_n_data(file_descriptor, &major, sizeof(major)) &&
|
||||
send_n_data(file_descriptor, &minor, sizeof(minor));
|
||||
}
|
||||
|
||||
bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
|
||||
int compression_level, bool send_path) {
|
||||
return file_send_single_calls_with_skip(file, file_descriptor, use_metadata, compression_level,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
/* Client-side file send path. */
|
||||
|
||||
bool file_send_special(File* file, int file_descriptor, bool use_metadata);
|
||||
bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
|
||||
int compression_level, bool send_path);
|
||||
bool file_send_single_calls_with_skip(File* file, int file_descriptor, bool use_metadata,
|
||||
|
||||
@@ -56,6 +56,15 @@ typedef struct {
|
||||
int link_group;
|
||||
bool link_first;
|
||||
char* hardlink_target;
|
||||
/* Phase 4 special/devices: when `is_special` is true this entry is a device
|
||||
* or special node to be RECREATED on the destination (mknod/mkfifo) rather
|
||||
* than written from `data`. The concrete node kind is derived from the
|
||||
* metadata mode's S_IFMT bits (receiver-validated), and rdev_major/minor
|
||||
* carry the device major/minor numbers for char/block devices. CROSSES the
|
||||
* wire (protocol 2.13.0). */
|
||||
bool is_special;
|
||||
int32_t rdev_major;
|
||||
int32_t rdev_minor;
|
||||
} File;
|
||||
|
||||
/* The path that should be sent on the wire and used for the receiver-side
|
||||
|
||||
@@ -308,8 +308,9 @@ int write_thread(void* pipeline_context) {
|
||||
}
|
||||
/* Record the per-file outcome so a --remove-source-files sender learns
|
||||
which sources were actually written versus skipped on the receiver.
|
||||
Explicit directory entries have no source and are never acknowledged. */
|
||||
if (context->config->remove_source_files && !file->is_dir && !file->skip &&
|
||||
Explicit directory entries and recreated device/special nodes have no
|
||||
source and are never acknowledged (mirrors receiver.c). */
|
||||
if (context->config->remove_source_files && !file->is_dir && !file->is_special && !file->skip &&
|
||||
!receiver_outcomes_append(&context->outcomes, (unsigned char)result)) {
|
||||
file_destroy(file);
|
||||
pipeline_context_receiver_note_bytes_released(context, file_bytes);
|
||||
|
||||
@@ -90,7 +90,14 @@ enum NET_STATUS {
|
||||
* first (data-carrying) member's destination-relative wire path; the receiver
|
||||
* creates this entry as a hard link to the first member's installed file
|
||||
* (falling back to a byte-identical copy if link() fails). Protocol 2.12.0. */
|
||||
STATUS_HARDLINK
|
||||
STATUS_HARDLINK,
|
||||
/* --devices / --specials (-D): a device or special node the sender wants
|
||||
* recreated (not written from content). Payload: destination path, the
|
||||
* metadata frame (whose mode's S_IFMT bits carry the node kind), and two
|
||||
* int32 rdev major/minor fields. The receiver validates the kind and rdev,
|
||||
* confines the node below the receive root, and recreates it (mknod/mkfifo),
|
||||
* privilege-gating the mknod. Protocol 2.13.0. */
|
||||
STATUS_SPECIAL
|
||||
};
|
||||
|
||||
void io_set_fds(int read_fd, int write_fd);
|
||||
|
||||
Reference in New Issue
Block a user