Merge feat/p4-devices
# Conflicts: # src/client/client_send.c # src/server/receiver.c # src/shared/chunk.c # src/shared/file.c # src/shared/file_receive.c # src/shared/file_receive.h # src/shared/file_types.h # src/shared/protocol.h # tests/test_chunk.c
This commit is contained in:
+48
-3
@@ -75,10 +75,17 @@ static unsigned long long per_file_serialize_size(File* file, bool use_metadata)
|
||||
return 0;
|
||||
size += metadata_size;
|
||||
/* Entry type marker: 0 = regular file, 1 = explicit directory entry,
|
||||
2 = symlink entry (carries its target string). */
|
||||
2 = symlink entry (carries its target string), 3 = 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);
|
||||
@@ -128,10 +135,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_symlink ? 2 : (file->is_dir ? 1 : 0);
|
||||
int entry_type = file->is_dir ? 1 : (file->is_symlink ? 2 : (file->is_special ? 3 : 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);
|
||||
|
||||
@@ -227,7 +243,7 @@ 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 && entry_type != 2) {
|
||||
if (entry_type != 0 && entry_type != 1 && entry_type != 2 && entry_type != 3) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: bad entry type");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
@@ -235,9 +251,38 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
}
|
||||
file->is_dir = entry_type == 1;
|
||||
file->is_symlink = entry_type == 2;
|
||||
file->is_special = entry_type == 3;
|
||||
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
@@ -79,6 +79,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;
|
||||
@@ -183,16 +186,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) &&
|
||||
@@ -448,12 +453,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) {
|
||||
@@ -573,7 +582,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;
|
||||
|
||||
@@ -124,6 +124,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;
|
||||
|
||||
@@ -116,6 +116,9 @@ File* file_create(const char* path) {
|
||||
file->hardlink_target = NULL;
|
||||
file->is_symlink = false;
|
||||
file->symlink_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
|
||||
@@ -1964,6 +2198,70 @@ File* file_receive_symlink(int file_descriptor, const Config* config) {
|
||||
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
|
||||
|
||||
@@ -11,6 +11,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_symlink(int file_descriptor, const Config* config);
|
||||
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,
|
||||
|
||||
@@ -63,6 +63,15 @@ typedef struct {
|
||||
* data. `data` is empty for a symlink entry. Sender + receiver state. */
|
||||
bool is_symlink;
|
||||
char* symlink_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);
|
||||
|
||||
@@ -96,7 +96,14 @@ enum NET_STATUS {
|
||||
* if --munge-links) symlink target, and optional metadata; the receiver
|
||||
* creates a symlink to the unmunged target beneath the receive root (see
|
||||
* file_receive_symlink). Protocol 2.13.0. */
|
||||
STATUS_SYMLINK
|
||||
STATUS_SYMLINK,
|
||||
/* --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