Merge PR #329: quality cycle
CI / lint (push) Successful in 1m52s
CI / parity-fast (push) Skipped
CI / parity-full (push) Successful in 22s
CI / sanitizers (address) (push) Successful in 55s
CI / sanitizers (undefined) (push) Successful in 46s
CI / build-and-test (push) Successful in 1m16s
CI / fuzz-build (push) Successful in 56s
CI / coverage (push) Successful in 46s
CI / valgrind (push) Successful in 2m35s

This commit was merged in pull request #329.
This commit is contained in:
2026-09-24 03:17:42 +02:00
23 changed files with 1585 additions and 1204 deletions
+12 -8
View File
File diff suppressed because one or more lines are too long
+4 -27
View File
@@ -64,16 +64,8 @@ bool add_chunk_to_manifest(ArrayList* manifest, const Chunk* chunk) {
int send_dry_run_manifest(const Config* config) { int send_dry_run_manifest(const Config* config) {
int skipped = 0; int skipped = 0;
ArrayList* missing_dest = NULL; ArrayList* missing_dest = NULL;
if (config->delete_missing_args) { if (!client_prepare_files_from(config, &missing_dest, &skipped))
missing_dest = array_list_create(free);
if (!missing_dest)
return -1;
}
if (!files_from_list_check(config, missing_dest, &skipped)) {
if (missing_dest)
array_list_delete(missing_dest);
return -1; return -1;
}
PreparedScanner prepared; PreparedScanner prepared;
if (!prepare_scanner(config, 0, &prepared)) { if (!prepare_scanner(config, 0, &prepared)) {
if (missing_dest) if (missing_dest)
@@ -466,33 +458,18 @@ bool send_delete_manifest_early(Client* client, ArrayList* manifest, ArrayList*
int send_dry_run_remote(Config* config) { int send_dry_run_remote(Config* config) {
int from_skipped = 0; int from_skipped = 0;
ArrayList* missing_args = NULL; ArrayList* missing_args = NULL;
if (config->delete_missing_args) { if (!client_prepare_files_from(config, &missing_args, &from_skipped))
missing_args = array_list_create(free);
if (!missing_args)
return 1;
}
if (!files_from_list_check(config, missing_args, &from_skipped)) {
if (missing_args)
array_list_delete(missing_args);
return 1; return 1;
}
if (missing_args) if (missing_args)
array_list_delete(missing_args); array_list_delete(missing_args);
/* A live session may follow, so arm graceful abort handling. */ /* A live session may follow, so arm graceful abort handling. */
client_set_abort_armed(true); client_set_abort_armed(true);
Client* client = connect_transfer_client(config); ProtocolSession session;
Client* client = client_connect_and_bind_session(config, &session);
if (!client) { if (!client) {
if (config->transport == TRANSPORT_TCP)
log_message(LOG_LEVEL_ERROR, "could not connect to server%s",
config->use_tls ? " via TLS" : "");
client_set_abort_armed(false); client_set_abort_armed(false);
return 1; return 1;
} }
ProtocolSession session;
protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
protocol_session_set_io_timeout(&session, config->timeout);
protocol_session_set_ssl(&session, (SSL*)client->ssl);
protocol_session_bind(&session);
int ret = 1; int ret = 1;
bool partial = false; bool partial = false;
+52 -36
View File
@@ -136,6 +136,50 @@ void disconnect_transfer_client(Client* client) {
client_delete(client); client_delete(client);
} }
/* Connect the configured transport and install the per-thread protocol session
* on it: init with the socket fd pair, apply the I/O timeout and (when
* negotiated) the TLS object, then bind it to this thread. Returns the
* connected client, or NULL (after logging the connect failure) when the
* transport could not connect. */
Client* client_connect_and_bind_session(const Config* config, ProtocolSession* session) {
Client* client = connect_transfer_client(config);
if (!client) {
if (config->transport == TRANSPORT_TCP)
log_message(LOG_LEVEL_ERROR, "could not connect to server%s",
config->use_tls ? " via TLS" : "");
return NULL;
}
protocol_session_init(session, client->file_descriptor, client->file_descriptor);
protocol_session_set_io_timeout(session, config->timeout);
protocol_session_set_ssl(session, (SSL*)client->ssl);
protocol_session_bind(session);
return client;
}
/* Shared --files-from/--delete-missing-args preamble: allocate the missing-args
* destination list when the option is set, then validate the --files-from list
* (collecting the destination mirrors of missing entries for the receiver's
* exact-deletion request). On success the caller owns *missing_args_out (NULL
* when the option is off); on failure the list is freed and false is returned. */
bool client_prepare_files_from(const Config* config, ArrayList** missing_args_out,
int* skipped_out) {
ArrayList* missing_args = NULL;
if (config->delete_missing_args) {
missing_args = array_list_create(free);
if (!missing_args)
return false;
}
int skipped = 0;
if (!files_from_list_check(config, missing_args, &skipped)) {
if (missing_args)
array_list_delete(missing_args);
return false;
}
*missing_args_out = missing_args;
*skipped_out = skipped;
return true;
}
/* (finalize_transfer is defined after the SourceFile helpers below.) */ /* (finalize_transfer is defined after the SourceFile helpers below.) */
typedef struct SourceFile { typedef struct SourceFile {
@@ -1039,20 +1083,13 @@ static int send_chunk_with_removal(Client* client, Chunk* chunk, Config* config,
static int send_chunks_multithreaded(void* pipeline_context) { static int send_chunks_multithreaded(void* pipeline_context) {
PipelineContextSender* context = (PipelineContextSender*)pipeline_context; PipelineContextSender* context = (PipelineContextSender*)pipeline_context;
time_t start = time(NULL); time_t start = time(NULL);
Client* client = connect_transfer_client(context->config); ProtocolSession session;
Client* client = client_connect_and_bind_session(context->config, &session);
if (!client) { if (!client) {
if (context->config->transport == TRANSPORT_TCP)
log_message(LOG_LEVEL_ERROR, "could not connect to server%s",
context->config->use_tls ? " via TLS" : "");
pipeline_cancel(context); pipeline_cancel(context);
mark_sender_done(context); mark_sender_done(context);
return thrd_error; return thrd_error;
} }
ProtocolSession session;
protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
protocol_session_set_io_timeout(&session, context->config->timeout);
protocol_session_set_ssl(&session, (SSL*)client->ssl);
protocol_session_bind(&session);
client_messages_activate(true); client_messages_activate(true);
if (!config_send(client->file_descriptor, context->config)) { if (!config_send(client->file_descriptor, context->config)) {
pipeline_cancel(context); pipeline_cancel(context);
@@ -2035,35 +2072,20 @@ static int send_files_impl(Config* config) {
shielded -- rsync's `-d DIR/ --delete`. */ shielded -- rsync's `-d DIR/ --delete`. */
state.delete_per_dir = config->use_delete && config_delete_timing_per_dir(config); state.delete_per_dir = config->use_delete && config_delete_timing_per_dir(config);
int skipped = 0; int skipped = 0;
if (config->delete_missing_args) { if (!client_prepare_files_from(config, &state.missing_args, &skipped))
state.missing_args = array_list_create(free);
if (!state.missing_args)
return 1;
}
if (!files_from_list_check(config, state.missing_args, &skipped)) {
if (state.missing_args)
array_list_delete(state.missing_args);
return 1; return 1;
}
/* From here on a server session may be live, so Ctrl-C/SIGTERM should set the /* From here on a server session may be live, so Ctrl-C/SIGTERM should set the
abort flag (and be forwarded as STATUS_ABORT) instead of terminating. */ abort flag (and be forwarded as STATUS_ABORT) instead of terminating. */
client_set_abort_armed(true); client_set_abort_armed(true);
Client* client = connect_transfer_client(config); ProtocolSession session;
Client* client = client_connect_and_bind_session(config, &session);
if (!client) { if (!client) {
if (config->transport == TRANSPORT_TCP)
log_message(LOG_LEVEL_ERROR, "could not connect to server%s",
config->use_tls ? " via TLS" : "");
if (state.missing_args) if (state.missing_args)
array_list_delete(state.missing_args); array_list_delete(state.missing_args);
return 1; return 1;
} }
state.client = client; state.client = client;
ProtocolSession session;
protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
protocol_session_set_io_timeout(&session, config->timeout);
protocol_session_set_ssl(&session, (SSL*)client->ssl);
protocol_session_bind(&session);
client_messages_activate(true); client_messages_activate(true);
int ret = 1; int ret = 1;
@@ -2099,16 +2121,8 @@ static int send_files_multithreaded_impl(Config* config) {
: send_dry_run_manifest(config); : send_dry_run_manifest(config);
ArrayList* missing_args = NULL; ArrayList* missing_args = NULL;
int skipped = 0; int skipped = 0;
if (config->delete_missing_args) { if (!client_prepare_files_from(config, &missing_args, &skipped))
missing_args = array_list_create(free);
if (!missing_args)
return 1;
}
if (!files_from_list_check(config, missing_args, &skipped)) {
if (missing_args)
array_list_delete(missing_args);
return 1; return 1;
}
/* Armed only once a session may go live (see send_files). */ /* Armed only once a session may go live (see send_files). */
client_set_abort_armed(true); client_set_abort_armed(true);
@@ -2137,6 +2151,8 @@ static int send_files_multithreaded_impl(Config* config) {
queue_destroy(q1); queue_destroy(q1);
if (q2) if (q2)
queue_destroy(q2); queue_destroy(q2);
if (missing_args)
array_list_delete(missing_args);
return 1; return 1;
} }
PipelineContextSender* context = pipeline_context_sender_create(config, q1, q2); PipelineContextSender* context = pipeline_context_sender_create(config, q1, q2);
+15
View File
@@ -13,6 +13,7 @@
#include "delta.h" #include "delta.h"
#include "format.h" #include "format.h"
#include "log.h" #include "log.h"
#include "protocol.h"
#include "scanner.h" #include "scanner.h"
#include <stdatomic.h> #include <stdatomic.h>
#include <stdbool.h> #include <stdbool.h>
@@ -91,6 +92,20 @@ void client_messages_end(void);
/* client_send.c */ /* client_send.c */
void receive_daemon_motd(Client* client, const Config* config); void receive_daemon_motd(Client* client, const Config* config);
Client* connect_transfer_client(const Config* config); Client* connect_transfer_client(const Config* config);
/* Connect the configured transport and install `session` on it: init with the
* socket fd pair, apply the I/O timeout and (when negotiated) the TLS object,
* then bind the session to this thread. Returns the connected client, or NULL
* after logging the connect failure. The caller owns the client and must keep
* `session` alive until it calls protocol_session_unbind(). */
Client* client_connect_and_bind_session(const Config* config, ProtocolSession* session);
/* Shared --files-from/--delete-missing-args preamble for the send entry points:
* when --delete-missing-args is set, allocate the list that
* files_from_list_check fills with the destination mirrors of missing entries;
* then validate the --files-from list. On success returns true and stores the
* (possibly NULL) owned list in *missing_args_out plus the skipped count; on
* failure returns false after freeing the list. */
bool client_prepare_files_from(const Config* config, ArrayList** missing_args_out,
int* skipped_out);
void disconnect_transfer_client(Client* client); void disconnect_transfer_client(Client* client);
int incremental_check(Client* client, File* file, const Config* config, DeltaSignature** out_sig, int incremental_check(Client* client, File* file, const Config* config, DeltaSignature** out_sig,
unsigned long long* resume_offset); unsigned long long* resume_offset);
+14 -54
View File
@@ -149,7 +149,7 @@ DirectoryScanner* directory_scanner_create_with_options(const char* root_directo
scanner->options = *options; scanner->options = *options;
if (scanner->options.chunk_size == 0) if (scanner->options.chunk_size == 0)
scanner->options.chunk_size = DESIRED_CHUNK_SIZE; scanner->options.chunk_size = DESIRED_CHUNK_SIZE;
scanner->directories = queue_create(100, dir_entry_destroy); scanner->directories = queue_create(SCANNER_RESULT_QUEUE_CAP, dir_entry_destroy);
if (!scanner->directories) { if (!scanner->directories) {
free(scanner); free(scanner);
return NULL; return NULL;
@@ -1026,7 +1026,7 @@ static ScannerAction scanner_process_entry(DirectoryScanner* scanner, ArrayList*
Chunk** out_chunk) { Chunk** out_chunk) {
const char* name = sorted->name; const char* name = sorted->name;
ScannerEntry* inspected = &sorted->entry; ScannerEntry* inspected = &sorted->entry;
char* cur_path = inspected->path; const char* cur_path = inspected->path;
struct stat stats = inspected->stats; struct stat stats = inspected->stats;
/* --files-from allow-set and the filter layer apply to files and to /* --files-from allow-set and the filter layer apply to files and to
@@ -1101,61 +1101,23 @@ static ScannerAction scanner_process_entry(DirectoryScanner* scanner, ArrayList*
free(rel_copy); free(rel_copy);
return SCANNER_ACTION_CONTINUE; return SCANNER_ACTION_CONTINUE;
} }
File* file = file_create(cur_path); /* Entry construction (data size, -R wire path, special/devices, hardlink
if (file == NULL) { group, metadata, xattrs) is shared with the parallel scanner. */
free(rel_copy); File* file = NULL;
free(inspected->link_target); bool build_failed = false;
inspected->link_target = NULL; ScannerBuildStatus status =
scanner_build_file_entry(&scanner->options, inspected, rel_copy, &file, &build_failed);
free(rel_copy);
rel_copy = NULL;
if (build_failed)
scanner->failed = true; scanner->failed = true;
if (status == SCANNER_BUILD_SKIP)
return SCANNER_ACTION_CONTINUE; return SCANNER_ACTION_CONTINUE;
} if (status != SCANNER_BUILD_OK) {
if (inspected->is_symlink) {
file->is_symlink = true;
file->symlink_target = inspected->link_target;
inspected->link_target = NULL;
} else {
file->data->size = stats.st_size;
}
if (scanner->relative_mode) {
file->send_path = rel_copy;
rel_copy = NULL;
} else if (scanner->options.relative_prefix) {
file->send_path = scanner_prefix_send_path(scanner->options.relative_prefix, rel_copy);
free(rel_copy);
rel_copy = NULL;
if (!file->send_path) {
file_destroy(file);
scanner->failed = true;
return SCANNER_ACTION_BREAK;
}
}
/* --devices/--specials: a device/FIFO/socket entry marked for preservation
becomes a node to recreate (is_special, no data, rdev captured); an
unrequested non-regular entry is skipped (rsync default). */
ScannerSpecial special =
scanner_prepare_special(scanner->options.preserve_devices, scanner->options.preserve_specials,
scanner->options.copy_devices, file, &stats);
if (special == SCANNER_SPECIAL_SKIP) {
scanner_note_nonreg(&scanner->options, file->path);
free(rel_copy);
file_destroy(file);
return SCANNER_ACTION_CONTINUE;
}
if (scanner->options.hardlinks && S_ISREG(stats.st_mode))
scanner_assign_hardlink(scanner, scanner->options.hardlinks, file, &stats);
if (scanner->options.use_metadata)
file->metadata = file_metadata_create(file->path, &stats, scanner->options.preserve_atimes,
scanner->options.preserve_crtimes);
if (scanner->options.use_metadata && !file->metadata) {
free(rel_copy);
file_destroy(file);
scanner->failed = true; scanner->failed = true;
return SCANNER_ACTION_BREAK; return status == SCANNER_BUILD_FAIL_CONTINUE ? SCANNER_ACTION_CONTINUE : SCANNER_ACTION_BREAK;
} }
if (!(file->link_group != 0 && !file->link_first))
scanner_capture_xattrs(scanner, file);
if (!array_list_add(chunk_data, file)) { if (!array_list_add(chunk_data, file)) {
free(rel_copy);
file_destroy(file); file_destroy(file);
scanner->failed = true; scanner->failed = true;
return SCANNER_ACTION_BREAK; return SCANNER_ACTION_BREAK;
@@ -1163,14 +1125,12 @@ static ScannerAction scanner_process_entry(DirectoryScanner* scanner, ArrayList*
scanner->current_dir_produced = true; scanner->current_dir_produced = true;
*chunk_data_size += file->data->size; *chunk_data_size += file->data->size;
if (*chunk_data_size > scanner->options.chunk_size) { if (*chunk_data_size > scanner->options.chunk_size) {
free(rel_copy);
Chunk* result = chunk_data_to_chunk(chunk_data); Chunk* result = chunk_data_to_chunk(chunk_data);
if (!result) if (!result)
scanner->failed = true; scanner->failed = true;
*out_chunk = result; *out_chunk = result;
return SCANNER_ACTION_CHUNK; return SCANNER_ACTION_CHUNK;
} }
free(rel_copy);
return SCANNER_ACTION_CONTINUE; return SCANNER_ACTION_CONTINUE;
} }
+5
View File
@@ -19,6 +19,11 @@
* keeps one transfer from spawning an unbounded pool on a very large machine. */ * keeps one transfer from spawning an unbounded pool on a very large machine. */
#define MAX_SCANNER_THREADS 256 #define MAX_SCANNER_THREADS 256
/* Depth of the scanner's work queues: the sequential scanner's pending-directory
* stack and the parallel scanner's result queue. Bounds memory for a very wide
* or very deep tree while leaving ample headroom for normal scans. */
#define SCANNER_RESULT_QUEUE_CAP 100
typedef struct { typedef struct {
bool use_metadata; bool use_metadata;
/* Phase 4 metadata capture: -U/--atimes and -N/--crtimes tell the scanner to /* Phase 4 metadata capture: -U/--atimes and -N/--crtimes tell the scanner to
+88 -15
View File
@@ -285,32 +285,34 @@ bool entry_passes_selection(const FileListSet* file_list, const FilterRuleList*
* read xattrs is non-fatal: the file is transferred without them. A symlink * read xattrs is non-fatal: the file is transferred without them. A symlink
* entry reads the LINK's own xattrs (never the referent's) with the no-follow * entry reads the LINK's own xattrs (never the referent's) with the no-follow
* variant; on Linux the VFS refuses xattrs on symlinks, so that yields NULL. */ * variant; on Linux the VFS refuses xattrs on symlinks, so that yields NULL. */
void scanner_capture_xattrs(const DirectoryScanner* scanner, File* file) { void scanner_capture_xattrs_opts(const ScannerOptions* options, File* file) {
if (!scanner || !file || !(scanner->options.preserve_xattrs || scanner->options.preserve_acls)) if (!options || !file || !(options->preserve_xattrs || options->preserve_acls))
return; return;
file->xattrs = file->is_symlink file->xattrs = file->is_symlink ? xattr_capture_path_nofollow(file->path, options->preserve_acls)
? xattr_capture_path_nofollow(file->path, scanner->options.preserve_acls) : xattr_capture_path(file->path, options->preserve_acls);
: xattr_capture_path(file->path, scanner->options.preserve_acls); }
void scanner_capture_xattrs(const DirectoryScanner* scanner, File* file) {
if (!scanner)
return;
scanner_capture_xattrs_opts(&scanner->options, file);
} }
/* Apply --hard-links (-H) detection to one regular File. On a sibling (a /* Apply --hard-links (-H) detection to one regular File. On a sibling (a
* later member of an already-seen source inode) the File keeps the group id * later member of an already-seen source inode) the File keeps the group id
* and the first member's wire path but carries NO data payload (size 0); the * and the first member's wire path but carries NO data payload (size 0); the
* first member is left untouched (data present, link_first). Allocation * first member is left untouched (data present, link_first). Returns false on
* failure is fatal: the scanner is marked failed. */ * allocation failure (the caller marks the scan failed); the File stays usable
void scanner_assign_hardlink(DirectoryScanner* scanner, HardLinkTable* table, File* file, * either way. */
const struct stat* stats) { bool scanner_assign_hardlink(HardLinkTable* table, File* file, const struct stat* stats) {
if (!table || !file || !stats) if (!table || !file || !stats)
return; return true;
int gid; int gid;
bool is_first; bool is_first;
char* first_path = NULL; char* first_path = NULL;
if (!hardlink_table_assign(table, file_wire_path(file), stats->st_dev, stats->st_ino, &gid, if (!hardlink_table_assign(table, file_wire_path(file), stats->st_dev, stats->st_ino, &gid,
&is_first, &first_path)) { &is_first, &first_path))
if (scanner) return false;
scanner->failed = true;
return;
}
file->link_group = gid; file->link_group = gid;
file->link_first = is_first; file->link_first = is_first;
if (!is_first) { if (!is_first) {
@@ -319,6 +321,7 @@ void scanner_assign_hardlink(DirectoryScanner* scanner, HardLinkTable* table, Fi
} else { } else {
free(first_path); free(first_path);
} }
return true;
} }
/* Phase 4 special/devices decision for one non-regular entry, matching rsync: /* Phase 4 special/devices decision for one non-regular entry, matching rsync:
@@ -399,6 +402,76 @@ void scanner_note_nonreg(const ScannerOptions* options, const char* fs_path) {
fflush(stdout); fflush(stdout);
} }
/* Construct one non-directory File from an inspected entry. Shared by the
* sequential and parallel scanners so entry construction has a single
* implementation: data size (or carried symlink), -R wire path, special/devices
* classification, hardlink group, metadata and xattr capture all happen here in
* the same order for both. See the declaration for the ownership contract. */
ScannerBuildStatus scanner_build_file_entry(const ScannerOptions* options, ScannerEntry* inspected,
const char* rel, File** out_file, bool* failed) {
*out_file = NULL;
if (failed)
*failed = false;
File* file = file_create(inspected->path);
if (!file) {
/* The File never existed, so drop the not-yet-transferred symlink target
here; the caller's entry teardown would otherwise double-free it. */
free(inspected->link_target);
inspected->link_target = NULL;
return SCANNER_BUILD_FAIL_CONTINUE;
}
if (inspected->is_symlink) {
file->is_symlink = true;
file->symlink_target = inspected->link_target;
inspected->link_target = NULL;
} else {
file->data->size = inspected->stats.st_size;
}
/* -R + --files-from uses the bare transfer-relative path; -R without
--files-from prefixes it. Plain scans keep the source path. */
bool relative_mode = options->relative && options->file_list != NULL;
if (relative_mode) {
file->send_path = str_dup(rel);
} else if (options->relative_prefix) {
file->send_path = scanner_prefix_send_path(options->relative_prefix, rel);
}
if ((relative_mode || options->relative_prefix) && !file->send_path) {
file_destroy(file);
return SCANNER_BUILD_FAIL_BREAK;
}
/* --devices/--specials: a device/FIFO/socket entry marked for preservation
becomes a node to recreate (is_special, no data, rdev captured); an
unrequested non-regular entry is skipped (rsync default). */
ScannerSpecial special =
scanner_prepare_special(options->preserve_devices, options->preserve_specials,
options->copy_devices, file, &inspected->stats);
if (special == SCANNER_SPECIAL_SKIP) {
scanner_note_nonreg(options, file->path);
file_destroy(file);
return SCANNER_BUILD_SKIP;
}
if (options->hardlinks && S_ISREG(inspected->stats.st_mode) &&
!scanner_assign_hardlink(options->hardlinks, file, &inspected->stats)) {
/* Allocation failure is non-fatal to this entry (it is still emitted) but
marks the scan failed, matching the historical inlined behaviour. */
if (failed)
*failed = true;
}
if (options->use_metadata) {
file->metadata = file_metadata_create(file->path, &inspected->stats, options->preserve_atimes,
options->preserve_crtimes);
if (!file->metadata) {
file_destroy(file);
return SCANNER_BUILD_FAIL_BREAK;
}
}
/* A hardlink sibling carries no data, so it carries no xattrs. */
if (!(file->link_group != 0 && !file->link_first))
scanner_capture_xattrs_opts(options, file);
*out_file = file;
return SCANNER_BUILD_OK;
}
/* rsync 3.4.1's `--info=mount` line, emitted when `-xx` drops a mount-point /* rsync 3.4.1's `--info=mount` line, emitted when `-xx` drops a mount-point
* directory: `[sender] skipping mount-point dir NAME` (the client is the * directory: `[sender] skipping mount-point dir NAME` (the client is the
* sender). Plain `-x` keeps the empty directory and prints nothing, matching * sender). Plain `-x` keeps the empty directory and prints nothing, matching
+24 -2
View File
@@ -62,6 +62,17 @@ typedef enum {
SCANNER_SPECIAL_SKIP, /* non-regular entry not requested: skip */ SCANNER_SPECIAL_SKIP, /* non-regular entry not requested: skip */
} ScannerSpecial; } ScannerSpecial;
/* Result of scanner_build_file_entry(). The two failure variants preserve the
* sequential scanner's historical distinction between a failure before the
* File existed (which kept walking the directory) and one afterwards (which cut
* the chunk short); both mark the scan failed. */
typedef enum {
SCANNER_BUILD_OK, /* File built; caller owns it */
SCANNER_BUILD_SKIP, /* non-regular entry not preserved; no File */
SCANNER_BUILD_FAIL_CONTINUE, /* failed before the File existed */
SCANNER_BUILD_FAIL_BREAK, /* failed after the File existed */
} ScannerBuildStatus;
/* scanner_filter.c */ /* scanner_filter.c */
void filter_node_destroy(void* item); void filter_node_destroy(void* item);
FilterNode* filter_node_alloc(FilterNode* parent, FilterRuleList* own); FilterNode* filter_node_alloc(FilterNode* parent, FilterRuleList* own);
@@ -79,10 +90,21 @@ bool entry_passes_selection(const FileListSet* file_list, const FilterRuleList*
const FilterNode* node, const char* rel, const char* leaf, bool is_dir, const FilterNode* node, const char* rel, const char* leaf, bool is_dir,
bool per_dir_filters, bool exclude_filter_files, bool* protect_out); bool per_dir_filters, bool exclude_filter_files, bool* protect_out);
void scanner_capture_xattrs(const DirectoryScanner* scanner, File* file); void scanner_capture_xattrs(const DirectoryScanner* scanner, File* file);
void scanner_assign_hardlink(DirectoryScanner* scanner, HardLinkTable* table, File* file, void scanner_capture_xattrs_opts(const ScannerOptions* options, File* file);
const struct stat* stats); bool scanner_assign_hardlink(HardLinkTable* table, File* file, const struct stat* stats);
ScannerSpecial scanner_prepare_special(bool preserve_devices, bool preserve_specials, ScannerSpecial scanner_prepare_special(bool preserve_devices, bool preserve_specials,
bool copy_devices, File* file, const struct stat* stats); bool copy_devices, File* file, const struct stat* stats);
/* Build one non-directory transfer File from an inspected entry. `rel` is the
* entry's transfer-root-relative path (used for the -R wire path); `inspected`
* supplies the on-disk path, stats and (for a carried symlink) the target whose
* ownership transfers to the File. Populates data size, send_path, special-node
* state, hardlink group, metadata and xattrs. On SCANNER_BUILD_OK the caller
* owns *out_file; on SCANNER_BUILD_SKIP it is NULL and the entry is dropped; on
* either failure it is NULL and the caller must mark the scan failed. `*failed`
* additionally reports a non-fatal hardlink-table allocation failure, in which
* case a usable File is still returned. */
ScannerBuildStatus scanner_build_file_entry(const ScannerOptions* options, ScannerEntry* inspected,
const char* rel, File** out_file, bool* failed);
bool excluded_sink_append(ArrayList* list, mtx_t* mtx, const char* rel); bool excluded_sink_append(ArrayList* list, mtx_t* mtx, const char* rel);
void scanner_note_nonreg(const ScannerOptions* options, const char* fs_path); void scanner_note_nonreg(const ScannerOptions* options, const char* fs_path);
void scanner_note_mount(const ScannerOptions* options, const char* fs_path); void scanner_note_mount(const ScannerOptions* options, const char* fs_path);
+168 -193
View File
@@ -109,7 +109,7 @@ static void parallel_scanner_creation_failed(ParallelScanner* ps) {
/* Initialize result queue and synchronization primitives. Returns true on success. */ /* Initialize result queue and synchronization primitives. Returns true on success. */
static bool parallel_scanner_init(ParallelScanner* ps) { static bool parallel_scanner_init(ParallelScanner* ps) {
ps->result_queue = queue_create(100, chunk_destroy); ps->result_queue = queue_create(SCANNER_RESULT_QUEUE_CAP, chunk_destroy);
if (!ps->result_queue) if (!ps->result_queue)
return false; return false;
atomic_init(&ps->cancelled, false); atomic_init(&ps->cancelled, false);
@@ -210,6 +210,157 @@ static Chunk* batch_files(ArrayList* files, unsigned long long chunk_size, Queue
return first; return first;
} }
/* Record the delete-protection mirror of a root entry that
* scanner_inspect_entry() skipped (inspection == 0): a dereferenced symlink
* with no referent is a partial-transfer I/O error and a user-selection or size
* prune protects the entry's destination mirror. */
static void scan_root_record_skipped(const ScannerOptions* options, const char* root_directory,
const char* name, const ScannerEntry* inspected,
ParallelScanner* ps) {
if (inspected->referent_error)
ps->io_error = true;
ArrayList* sink = NULL;
if (inspected->excluded)
sink = inspected->size_excluded ? options->size_skipped_paths : options->excluded_paths;
if (!sink)
return;
/* A root-level prune protects the destination mirror of the entry's wire
path: under -R + --files-from that is the bare relative name, otherwise it
is the full source path with a leading '/' removed (matching the
send_path/file_wire_path the scanner hands the sender). */
if (options->relative && options->file_list != NULL) {
if (!excluded_sink_append(sink, options->excluded_mutex, name))
ps->failed = true;
} else if (options->relative_prefix) {
char* wrel = scanner_prefix_send_path(options->relative_prefix, name);
if (!wrel) {
ps->failed = true;
} else {
if (!excluded_sink_append(sink, options->excluded_mutex, wrel))
ps->failed = true;
free(wrel);
}
} else {
char* abs_path = path_cat(root_directory, name);
if (!abs_path) {
ps->failed = true;
} else {
const char* rel = *abs_path == '/' ? abs_path + 1 : abs_path;
if (!excluded_sink_append(sink, options->excluded_mutex, rel))
ps->failed = true;
free(abs_path);
}
}
}
/* Record the delete-protection mirror of a root entry dropped by the
* --files-from allow-set or a filter rule. Returns false only when the -R
* prefix could not be built (the caller must abandon the entry immediately);
* other allocation failures mark the scan failed but let the caller continue to
* the filter-notice step, matching the historical inlined flow. */
static bool scan_root_record_protection(const ScannerOptions* options, const char* rel,
const char* name, const char* cur_path, bool protect,
bool passes, bool use_rel, ParallelScanner* ps) {
if (passes && !protect)
return true;
/* --files-from subset pruning is not a filter exclusion; -R bare-wire-path
exclusions are never recorded (see ScannerOptions.excluded_paths). */
bool files_from_prune = options->file_list && !file_list_affects(options->file_list, rel);
if ((!files_from_prune && !use_rel) || protect) {
const char* rel_path;
char* prefixed = NULL;
if (use_rel) {
/* -R + --files-from: the destination/wire path is the bare relative
name, not the source path. */
rel_path = rel;
} else if (options->relative_prefix) {
prefixed = scanner_prefix_send_path(options->relative_prefix, name);
if (!prefixed)
return false;
rel_path = prefixed;
} else {
rel_path = *cur_path == '/' ? cur_path + 1 : cur_path;
}
if (options->excluded_paths &&
!excluded_sink_append(options->excluded_paths, options->excluded_mutex, rel_path))
ps->failed = true;
free(prefixed);
}
return true;
}
/* Root-level directory node: apply -x/--one-file-system and either emit the
* mount-point directory (plain -x) or queue the directory for a worker. */
static void scan_root_dir(const ScannerOptions* options, const char* cur_path, const char* rel,
const struct stat* st, ArrayList* root_files, ArrayList* subdirs,
dev_t root_dev, ParallelScanner* ps) {
if (!scanner_same_filesystem(options->one_file_system, root_dev, st->st_dev)) {
if (options->one_file_system > 1) {
/* -xx: drop the mount-point directory entirely (rsync) and print the
--info=mount line when enabled. */
scanner_note_mount(options, cur_path);
return;
}
/* -x/--one-file-system: emit the mount-point directory entry (empty) but do
not descend into it (see the sequential scanner for the same rule). */
File* mount = scanner_build_dir_file(cur_path, st, options);
if (!mount) {
ps->failed = true;
return;
}
if (options->relative_prefix) {
mount->send_path = scanner_prefix_send_path(options->relative_prefix, rel);
if (!mount->send_path) {
file_destroy(mount);
ps->failed = true;
return;
}
}
if (!array_list_add(root_files, mount)) {
file_destroy(mount);
ps->failed = true;
}
return;
}
char* dir = str_dup(cur_path);
if (!dir || !array_list_add(subdirs, dir)) {
free(dir);
ps->failed = true;
}
}
/* Build a non-directory root entry through the shared construction path and add
* it to `root_files`. A non-regular entry the options do not preserve is
* dropped by the builder (which prints rsync's nonreg line); an allocation
* failure marks the scan failed. */
static void scan_root_add_non_dir(const ScannerOptions* options, ScannerEntry* inspected,
const char* rel, ArrayList* root_files, ParallelScanner* ps) {
File* file = NULL;
bool failed = false;
ScannerBuildStatus status = scanner_build_file_entry(options, inspected, rel, &file, &failed);
if (failed || status == SCANNER_BUILD_FAIL_CONTINUE || status == SCANNER_BUILD_FAIL_BREAK)
ps->failed = true;
if (status != SCANNER_BUILD_OK)
return;
if (!array_list_add(root_files, file)) {
file_destroy(file);
ps->failed = true;
}
}
/* Regular file or carried symlink at the transfer root. */
static void scan_root_file(const ScannerOptions* options, ScannerEntry* inspected, const char* rel,
ArrayList* root_files, ParallelScanner* ps) {
scan_root_add_non_dir(options, inspected, rel, root_files, ps);
}
/* Device/FIFO/socket at the transfer root: recreated under --devices/--specials,
* otherwise dropped by the shared builder. */
static void scan_root_special(const ScannerOptions* options, ScannerEntry* inspected,
const char* rel, ArrayList* root_files, ParallelScanner* ps) {
scan_root_add_non_dir(options, inspected, rel, root_files, ps);
}
/* Scan one root-directory entry into either the subdirs or files list. */ /* Scan one root-directory entry into either the subdirs or files list. */
static void scan_root_entry(const ScannerOptions* options, const FilterNode* root_node, static void scan_root_entry(const ScannerOptions* options, const FilterNode* root_node,
const char* root_directory, const struct dirent* entry, const char* root_directory, const struct dirent* entry,
@@ -223,51 +374,16 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
return; return;
} }
if (inspection == 0) { if (inspection == 0) {
if (inspected.referent_error) scan_root_record_skipped(options, root_directory, entry->d_name, &inspected, ps);
ps->io_error = true;
ArrayList* sink = NULL;
if (inspected.excluded)
sink = inspected.size_excluded ? options->size_skipped_paths : options->excluded_paths;
if (sink) {
/* A root-level prune protects the destination mirror of the entry's wire
path: under -R + --files-from that is the bare relative name, otherwise
it is the full source path with a leading '/' removed (matching the
send_path/file_wire_path the scanner hands the sender). */
if (options->relative && options->file_list != NULL) {
if (!excluded_sink_append(sink, options->excluded_mutex, entry->d_name))
ps->failed = true;
} else if (options->relative_prefix) {
char* wrel = scanner_prefix_send_path(options->relative_prefix, entry->d_name);
if (!wrel) {
ps->failed = true;
} else {
if (!excluded_sink_append(sink, options->excluded_mutex, wrel))
ps->failed = true;
free(wrel);
}
} else {
char* abs_path = path_cat(root_directory, entry->d_name);
if (!abs_path) {
ps->failed = true;
} else {
const char* rel = *abs_path == '/' ? abs_path + 1 : abs_path;
if (!excluded_sink_append(sink, options->excluded_mutex, rel))
ps->failed = true;
free(abs_path);
}
}
}
return; return;
} }
char* cur_path = inspected.path; char* cur_path = inspected.path;
struct stat st = inspected.stats;
bool is_dir = inspected.is_directory;
char* rel = str_dup(entry->d_name); char* rel = str_dup(entry->d_name);
if (!rel) { if (!rel) {
free(cur_path);
ps->failed = true; ps->failed = true;
return; goto done;
} }
bool is_dir = inspected.is_directory;
bool protect = false; bool protect = false;
bool passes = entry_passes_selection(options->file_list, options->base_filters, root_node, rel, bool passes = entry_passes_selection(options->file_list, options->base_filters, root_node, rel,
entry->d_name, is_dir, options->per_dir_filters, entry->d_name, is_dir, options->per_dir_filters,
@@ -275,169 +391,28 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
/* -R + --files-from: root-level files keep their bare relative send path. */ /* -R + --files-from: root-level files keep their bare relative send path. */
bool use_rel = options->relative && options->file_list != NULL; bool use_rel = options->relative && options->file_list != NULL;
if (!passes || protect) { if (!passes || protect) {
/* --files-from subset pruning is not a filter exclusion; -R bare-wire-path if (!scan_root_record_protection(options, rel, entry->d_name, cur_path, protect, passes,
exclusions are never recorded (see ScannerOptions.excluded_paths). */ use_rel, ps)) {
bool files_from_prune = options->file_list && !file_list_affects(options->file_list, rel); ps->failed = true;
if ((!files_from_prune && !use_rel) || protect) { goto done;
const char* rel_path;
char* prefixed = NULL;
if (use_rel) {
/* -R + --files-from: the destination/wire path is the bare relative
name, not the source path. */
rel_path = rel;
} else if (options->relative_prefix) {
prefixed = scanner_prefix_send_path(options->relative_prefix, entry->d_name);
if (!prefixed) {
free(rel);
free(cur_path);
ps->failed = true;
return;
}
rel_path = prefixed;
} else {
rel_path = *cur_path == '/' ? cur_path + 1 : cur_path;
}
if (options->excluded_paths &&
!excluded_sink_append(options->excluded_paths, options->excluded_mutex, rel_path))
ps->failed = true;
free(prefixed);
} }
if (!passes) { if (!passes) {
scanner_note_filter(options, entry->d_name); scanner_note_filter(options, entry->d_name);
free(rel); goto done;
free(cur_path);
return;
} }
} }
if (is_dir) { if (is_dir) {
if (!scanner_same_filesystem(options->one_file_system, root_dev, st.st_dev)) { scan_root_dir(options, cur_path, rel, &inspected.stats, root_files, subdirs, root_dev, ps);
if (options->one_file_system > 1) { } else if (S_ISCHR(inspected.stats.st_mode) || S_ISBLK(inspected.stats.st_mode) ||
/* -xx: drop the mount-point directory entirely (rsync) and print the S_ISFIFO(inspected.stats.st_mode) || S_ISSOCK(inspected.stats.st_mode)) {
--info=mount line when enabled. */ scan_root_special(options, &inspected, rel, root_files, ps);
scanner_note_mount(options, cur_path);
free(rel);
free(cur_path);
return;
}
/* -x/--one-file-system: emit the mount-point directory entry (empty) but
do not descend into it (see the sequential scanner for the same rule). */
File* mount = file_create(cur_path);
free(cur_path);
if (mount == NULL) {
free(rel);
ps->failed = true;
return;
}
mount->is_dir = true;
if (options->use_metadata) {
mount->metadata = file_metadata_create(mount->path, &st, options->preserve_atimes,
options->preserve_crtimes);
if (!mount->metadata) {
free(rel);
file_destroy(mount);
ps->failed = true;
return;
}
}
if (options->relative_prefix) {
mount->send_path = scanner_prefix_send_path(options->relative_prefix, rel);
if (!mount->send_path) {
free(rel);
file_destroy(mount);
ps->failed = true;
return;
}
}
free(rel);
if (!array_list_add(root_files, mount)) {
file_destroy(mount);
ps->failed = true;
}
return;
}
free(rel);
if (!array_list_add(subdirs, cur_path)) {
free(cur_path);
ps->failed = true;
}
return;
}
File* file = file_create(cur_path);
free(cur_path);
if (!file) {
free(rel);
free(inspected.link_target);
inspected.link_target = NULL;
ps->failed = true;
return;
}
if (inspected.is_symlink) {
file->is_symlink = true;
file->symlink_target = inspected.link_target;
inspected.link_target = NULL;
} else { } else {
file->data->size = st.st_size; scan_root_file(options, &inspected, rel, root_files, ps);
}
if (use_rel) {
file->send_path = rel;
rel = NULL;
} else if (options->relative_prefix) {
file->send_path = scanner_prefix_send_path(options->relative_prefix, rel);
free(rel);
rel = NULL;
if (!file->send_path) {
file_destroy(file);
ps->failed = true;
return;
}
}
ScannerSpecial special = scanner_prepare_special(
options->preserve_devices, options->preserve_specials, options->copy_devices, file, &st);
if (special == SCANNER_SPECIAL_SKIP) {
scanner_note_nonreg(ps->options, file->path);
free(rel);
file_destroy(file);
return;
}
if (options->hardlinks && S_ISREG(st.st_mode)) {
int gid;
bool is_first;
char* first_path = NULL;
if (!hardlink_table_assign((HardLinkTable*)options->hardlinks, file_wire_path(file), st.st_dev,
st.st_ino, &gid, &is_first, &first_path)) {
ps->failed = true;
} else {
file->link_group = gid;
file->link_first = is_first;
if (!is_first) {
file->hardlink_target = first_path;
file->data->size = 0;
} else {
free(first_path);
}
}
}
if (options->use_metadata)
file->metadata =
file_metadata_create(file->path, &st, options->preserve_atimes, options->preserve_crtimes);
if (options->use_metadata && !file->metadata) {
free(rel);
file_destroy(file);
ps->failed = true;
return;
}
if ((options->preserve_xattrs || options->preserve_acls) &&
!(file->link_group != 0 && !file->link_first))
file->xattrs = file->is_symlink
? xattr_capture_path_nofollow(file->path, options->preserve_acls)
: xattr_capture_path(file->path, options->preserve_acls);
if (!array_list_add(root_files, file)) {
free(rel);
file_destroy(file);
ps->failed = true;
return;
} }
done:
free(rel); free(rel);
free(cur_path);
free(inspected.link_target);
} }
/* Scan the root directory itself, collecting root files and subdirectories. /* Scan the root directory itself, collecting root files and subdirectories.
+284 -244
View File
@@ -1306,6 +1306,284 @@ static bool daemonize(void) {
return true; return true;
} }
/* Apply the process-wide policies shared by the stdio and listener
* entrypoints: logging verbosity, signal handling, the parsed server
* authorization policies, and the socket timeout floor. Runs after CLI
* parsing and after the standalone --hash-credentials tool has been ruled
* out. */
static void configure_server_process(const ServerCliOptions* opts) {
signal(SIGPIPE, SIG_IGN);
if (opts->verbose) {
set_log_level(LOG_LEVEL_DEBUG);
set_log_debug_flags(LOG_DEBUG_ALL);
}
if (opts->tls_ca && !opts->use_tls)
log_message(LOG_LEVEL_WARNING, "--ca has no effect without --tls");
/* Persist the parsed server policies into the process-global policy state
* BEFORE the stdio branch: an SSH-launched `--stdio` server (whose argv came
* from the client via --remote-option and friends) must honor --allow-delete,
* --trust-sender and --client-cn exactly like the standalone listener. */
required_client_cn = opts->client_cn;
allow_delete = opts->allow_delete;
trust_sender = opts->trust_sender;
allow_unauthenticated = opts->allow_unauthenticated;
server_no_super = opts->no_super;
/* --stdio rejects --allow-super at parse time; force it off here as well so
* this process-global policy cannot be re-enabled by a future caller. */
server_allow_super = opts->allow_super && !opts->stdio_mode;
server_iconv_spec = opts->iconv_spec;
install_cleanup_handler(SIGINT);
install_cleanup_handler(SIGTERM);
/* Server-owned socket deadline floor: the client default --timeout=0 would
* otherwise leave accepted sockets without SO_RCVTIMEO/SO_SNDTIMEO and let a
* silent peer hold a connection (and its process slot) forever. */
tcp_set_timeouts(SERVER_IO_TIMEOUT_SEC, SERVER_IO_TIMEOUT_SEC);
}
/* --hash-credentials: standalone offline tool; read user:password lines and
* emit new-format credential-store lines, then exit. Consumes and frees
* opts. */
static int run_hash_credentials_tool(ServerCliOptions* opts) {
uint32_t iters = opts->hash_iterations_set ? opts->hash_iterations : CREDENTIAL_DEFAULT_ITERS;
/* The output is secret material: if it is redirected to a regular file,
* warn when that file is group/other-accessible (the store must be 0600). */
struct stat out_st;
if (fstat(STDOUT_FILENO, &out_st) == 0 && S_ISREG(out_st.st_mode) &&
(out_st.st_mode & (S_IRWXG | S_IRWXO)) != 0)
fprintf(stderr,
"Warning: credential-store output is a group/other-accessible file; restrict it to "
"mode 0600 (chmod 600)\n");
char hash_err[512];
if (credentials_hash_file(opts->hash_credentials_file, iters, stdout, hash_err,
sizeof(hash_err)) != 0) {
fprintf(stderr, "Error: %s\n", hash_err);
server_cli_options_free(opts);
return 1;
}
server_cli_options_free(opts);
return 0;
}
/* SSH --stdio session: the transport is authenticated by sshd outside of
* FastSync, so the single connection is served over STDIN/STDOUT and the
* process exits. The destination root is authorized exactly like the listener
* path. Consumes and frees opts. */
static int run_stdio_server(ServerCliOptions* opts) {
/* SSH authenticates the stdio transport outside of FastSync. */
allow_unauthenticated = true;
if (!configure_authorization(opts->destination_root)) {
char* escaped = output_escape(opts->destination_root, false);
fprintf(stderr, "Error: invalid destination root '%s'\n",
escaped ? escaped : "<allocation failed>");
free(escaped);
server_cli_options_free(opts);
return 1;
}
io_set_fds(STDIN_FILENO, STDOUT_FILENO);
/* handler() does not own the stdio fds: it never closes its descriptor
* argument, so STDIN/STDOUT stay open for this (single-shot) SSH session
* and are released by process exit. */
handler(STDIN_FILENO);
release_authorization();
server_cli_options_free(opts);
return 0;
}
/* Load the daemon config, apply --dparam overrides, resolve the effective
* port/address, and surface the operator-facing module warnings. On failure
* the error is printed and false is returned. */
static bool load_daemon_policy(ServerCliOptions* opts, int* port, const char** bind_address,
char* err, size_t err_size) {
const char* config_path = opts->config_path ? opts->config_path : default_daemon_config_path();
g_daemon_conf = daemon_conf_load(config_path, err, err_size);
if (!g_daemon_conf) {
fprintf(stderr, "Error: %s\n", err);
return false;
}
for (int i = 0; i < opts->dparam_count; i++) {
if (daemon_conf_apply_dparam(g_daemon_conf, opts->dparams[i], err, err_size) != 0) {
fprintf(stderr, "Error: --dparam: %s\n", err);
return false;
}
}
/* Effective port: -p (highest) > --dparam port > config port (default 873). */
if (!opts->port_set)
*port = g_daemon_conf->global.port;
if (!*bind_address)
*bind_address = g_daemon_conf->global.address;
if (g_daemon_conf->module_count == 0)
log_message(LOG_LEVEL_WARNING,
"daemon config has no modules; every connection will be refused");
/* Surface the operator's client-chosen-ownership opt-in prominently: an
opted-in module lets its clients request arbitrary owner ids inside that
module root. */
for (int i = 0; i < g_daemon_conf->module_count; i++) {
if (g_daemon_conf->modules[i].client_owner)
log_message(LOG_LEVEL_WARNING,
"daemon module '%s' allows client-chosen ownership and super-user device "
"activities (`client owner = yes`); clients may request arbitrary owner ids "
"and device nodes within that module root -- pair it with `auth users` "
"unless the module is intentionally open to the network",
g_daemon_conf->modules[i].name);
if (g_daemon_conf->modules[i].max_connections > 0)
log_message(LOG_LEVEL_INFO,
"daemon module '%s': per-module 'max connections' cap = %d (enforced "
"across all connection children)",
g_daemon_conf->modules[i].name, g_daemon_conf->modules[i].max_connections);
}
return true;
}
/* Load the daemon credential store (Wave B) and enforce the fail-closed
* startup check: a module that declares `auth users` without a store (or with
* an empty store) refuses to start rather than serving a module whose
* credentials can never be verified. On failure the error is printed and
* false is returned. */
static bool validate_daemon_credentials(const ServerCliOptions* opts, char* err, size_t err_size) {
/* --password-file and --early-input feed the same store, loaded BEFORE the
* listener forks so every connection child shares one read-only store. */
g_credentials = credentials_load(opts->password_file, opts->early_input_file, err, err_size);
if (!g_credentials) {
fprintf(stderr, "Error: %s\n", err);
return false;
}
bool credential_source_given = opts->password_file != NULL || opts->early_input_file != NULL;
for (int i = 0; i < g_daemon_conf->module_count; i++) {
const DaemonModule* module = &g_daemon_conf->modules[i];
if (module->auth_user_count == 0)
continue;
if (!credential_source_given) {
fprintf(stderr,
"Error: module '%s' declares 'auth users' but no credential store was given "
"(--password-file or --early-input); refusing to start (fail closed)\n",
module->name);
return false;
}
if (credentials_store_size(g_credentials) == 0) {
fprintf(stderr,
"Error: module '%s' declares 'auth users' but the credential store is empty; "
"refusing to start (fail closed)\n",
module->name);
return false;
}
for (int j = 0; j < module->auth_user_count; j++) {
if (!credentials_store_has(g_credentials, module->auth_users[j]))
log_message(LOG_LEVEL_WARNING,
"daemon module '%s': auth user '%s' has no credential store entry; that "
"user can never authenticate",
module->name, module->auth_users[j]);
}
}
return true;
}
/* Create the shared cross-process registry for the per-module / per-source
* caps and the auth lockout. Called in the parent before any accept-loop fork;
* every connection child inherits the mapping. A failure degrades to
* "registry disabled" (the global cap and host ACLs still apply) rather than
* refusing to start. */
static void create_daemon_limits(void) {
g_daemon_limits = daemon_limits_create(
(int)g_daemon_conf->global.max_connections, g_daemon_conf->module_count,
g_daemon_conf->global.max_connections_per_host, g_daemon_conf->global.auth_lockout_threshold,
g_daemon_conf->global.auth_lockout_duration_sec);
if (!g_daemon_limits)
log_message(LOG_LEVEL_WARNING,
"daemon: could not allocate the shared connection registry; per-module / "
"per-host caps and the cross-process auth lockout are disabled (the global "
"'max connections' cap and host ACLs still apply)");
}
/* Bind the listener, apply the daemon caps, set up TLS when requested, detach
* when daemonizing, and run the accept loop. Returns the process exit code. */
static int start_listener(ServerCliOptions* opts, int port, int bind_family,
const char* bind_address) {
ServerBindOptions bind_opts;
bind_opts.bind_address = bind_address;
bind_opts.family = bind_family;
g_server = server_create_ex(port, &bind_opts);
if (!g_server) {
log_message(LOG_LEVEL_ERROR, "Failed to create server");
release_authorization();
return 1;
}
if (g_daemon_conf)
server_set_max_connections(g_server, (unsigned int)g_daemon_conf->global.max_connections);
if (g_daemon_limits)
server_set_limit_registry(g_server, g_daemon_limits);
if (opts->use_tls) {
if (!opts->tls_cert || !opts->tls_key || !opts->tls_ca || !opts->client_cn) {
fprintf(stderr, "Error: --tls requires --cert, --key, --ca, and --client-cn\n");
server_delete(&g_server);
release_authorization();
return 1;
}
tls_global_init();
if (!server_create_tls(g_server, opts->tls_cert, opts->tls_key, opts->tls_ca)) {
log_message(LOG_LEVEL_ERROR, "Failed to set up TLS");
server_delete(&g_server);
release_authorization();
return 1;
}
}
/* Detach after the listening socket (and TLS context) exist so the
* background daemon inherits a fully-bound listener. --no-detach runs in
* the foreground, which is how tests drive the daemon. */
if (opts->daemon_mode && !opts->no_detach) {
if (!daemonize()) {
log_message(LOG_LEVEL_ERROR, "Failed to daemonize");
server_delete(&g_server);
release_authorization();
return 1;
}
}
if (opts->use_tls)
server_listen_tls(g_server, handler);
else
server_listen(g_server, handler);
server_delete(&g_server);
release_authorization();
return 0;
}
/* Listener entrypoint: the daemon (config-driven, possibly detached) and the
* standalone TCP server share the same bind/TLS/listen path. Consumes and
* frees opts. */
static int run_daemon_server(ServerCliOptions* opts) {
int port = opts->port;
const char* bind_address = opts->bind_address;
char cli_err[512];
int exit_code = 0;
if (opts->daemon_mode) {
if (!load_daemon_policy(opts, &port, &bind_address, cli_err, sizeof(cli_err)) ||
!validate_daemon_credentials(opts, cli_err, sizeof(cli_err))) {
exit_code = 1;
goto out;
}
create_daemon_limits();
} else if (!configure_authorization(opts->destination_root)) {
char* escaped = output_escape(opts->destination_root, false);
fprintf(stderr, "Error: invalid destination root '%s'\n",
escaped ? escaped : "<allocation failed>");
free(escaped);
exit_code = 1;
goto out;
}
exit_code = start_listener(opts, port, opts->bind_family, bind_address);
out:
daemon_limits_destroy(g_daemon_limits);
g_daemon_limits = NULL;
daemon_conf_free(g_daemon_conf);
g_daemon_conf = NULL;
credentials_free(g_credentials);
g_credentials = NULL;
server_cli_options_free(opts);
return exit_code;
}
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
/* Capture the process umask now, while still single-threaded: the cached /* Capture the process umask now, while still single-threaded: the cached
* value is what file_mode_base() uses, and reading it later would race with * value is what file_mode_base() uses, and reading it later would race with
@@ -1325,250 +1603,12 @@ int main(int argc, char* argv[]) {
return 1; return 1;
} }
/* --hash-credentials: standalone offline tool; read user:password lines and if (opts.hash_credentials_file)
* emit new-format credential-store lines, then exit. */ return run_hash_credentials_tool(&opts);
if (opts.hash_credentials_file) {
uint32_t iters = opts.hash_iterations_set ? opts.hash_iterations : CREDENTIAL_DEFAULT_ITERS;
/* The output is secret material: if it is redirected to a regular file,
* warn when that file is group/other-accessible (the store must be 0600). */
struct stat out_st;
if (fstat(STDOUT_FILENO, &out_st) == 0 && S_ISREG(out_st.st_mode) &&
(out_st.st_mode & (S_IRWXG | S_IRWXO)) != 0)
fprintf(stderr,
"Warning: credential-store output is a group/other-accessible file; restrict it to "
"mode 0600 (chmod 600)\n");
char hash_err[512];
if (credentials_hash_file(opts.hash_credentials_file, iters, stdout, hash_err,
sizeof(hash_err)) != 0) {
fprintf(stderr, "Error: %s\n", hash_err);
server_cli_options_free(&opts);
return 1;
}
server_cli_options_free(&opts);
return 0;
}
int exit_code = 0; configure_server_process(&opts);
signal(SIGPIPE, SIG_IGN); if (opts.stdio_mode)
if (opts.verbose) { return run_stdio_server(&opts);
set_log_level(LOG_LEVEL_DEBUG); return run_daemon_server(&opts);
set_log_debug_flags(LOG_DEBUG_ALL);
}
if (opts.tls_ca && !opts.use_tls)
log_message(LOG_LEVEL_WARNING, "--ca has no effect without --tls");
/* Persist the parsed server policies into the process-global policy state
* BEFORE the stdio branch: an SSH-launched `--stdio` server (whose argv came
* from the client via --remote-option and friends) must honor --allow-delete,
* --trust-sender and --client-cn exactly like the standalone listener. */
required_client_cn = opts.client_cn;
allow_delete = opts.allow_delete;
trust_sender = opts.trust_sender;
allow_unauthenticated = opts.allow_unauthenticated;
server_no_super = opts.no_super;
/* --stdio rejects --allow-super at parse time; force it off here as well so
* this process-global policy cannot be re-enabled by a future caller. */
server_allow_super = opts.allow_super && !opts.stdio_mode;
server_iconv_spec = opts.iconv_spec;
install_cleanup_handler(SIGINT);
install_cleanup_handler(SIGTERM);
/* Server-owned socket deadline floor: the client default --timeout=0 would
* otherwise leave accepted sockets without SO_RCVTIMEO/SO_SNDTIMEO and let a
* silent peer hold a connection (and its process slot) forever. */
tcp_set_timeouts(SERVER_IO_TIMEOUT_SEC, SERVER_IO_TIMEOUT_SEC);
if (opts.stdio_mode) {
/* SSH authenticates the stdio transport outside of FastSync. */
allow_unauthenticated = true;
if (!configure_authorization(opts.destination_root)) {
char* escaped = output_escape(opts.destination_root, false);
fprintf(stderr, "Error: invalid destination root '%s'\n",
escaped ? escaped : "<allocation failed>");
free(escaped);
server_cli_options_free(&opts);
return 1;
}
io_set_fds(STDIN_FILENO, STDOUT_FILENO);
/* handler() does not own the stdio fds: it never closes its descriptor
* argument, so STDIN/STDOUT stay open for this (single-shot) SSH session
* and are released by process exit. */
handler(STDIN_FILENO);
release_authorization();
server_cli_options_free(&opts);
return 0;
}
int port = opts.port;
int bind_family = opts.bind_family;
const char* bind_address = opts.bind_address;
if (opts.daemon_mode) {
const char* config_path = opts.config_path ? opts.config_path : default_daemon_config_path();
g_daemon_conf = daemon_conf_load(config_path, cli_err, sizeof(cli_err));
if (!g_daemon_conf) {
server_cli_options_free(&opts);
fprintf(stderr, "Error: %s\n", cli_err);
return 1;
}
for (int i = 0; i < opts.dparam_count; i++) {
if (daemon_conf_apply_dparam(g_daemon_conf, opts.dparams[i], cli_err, sizeof(cli_err)) != 0) {
fprintf(stderr, "Error: --dparam: %s\n", cli_err);
exit_code = 1;
goto out;
}
}
/* Effective port: -p (highest) > --dparam port > config port (default 873). */
if (!opts.port_set)
port = g_daemon_conf->global.port;
if (!bind_address)
bind_address = g_daemon_conf->global.address;
if (g_daemon_conf->module_count == 0)
log_message(LOG_LEVEL_WARNING,
"daemon config has no modules; every connection will be refused");
/* Surface the operator's client-chosen-ownership opt-in prominently: an
opted-in module lets its clients request arbitrary owner ids inside that
module root. */
for (int i = 0; i < g_daemon_conf->module_count; i++) {
if (g_daemon_conf->modules[i].client_owner)
log_message(LOG_LEVEL_WARNING,
"daemon module '%s' allows client-chosen ownership and super-user device "
"activities (`client owner = yes`); clients may request arbitrary owner ids "
"and device nodes within that module root -- pair it with `auth users` "
"unless the module is intentionally open to the network",
g_daemon_conf->modules[i].name);
if (g_daemon_conf->modules[i].max_connections > 0)
log_message(LOG_LEVEL_INFO,
"daemon module '%s': per-module 'max connections' cap = %d (enforced "
"across all connection children)",
g_daemon_conf->modules[i].name, g_daemon_conf->modules[i].max_connections);
}
/* Daemon credential store (Wave B). --password-file and --early-input
* feed the same store, loaded BEFORE the listener forks so every
* connection child shares one read-only store. Fail closed at startup: a
* module that declares `auth users` without a store (or with an empty
* store) refuses to start rather than serving a module whose credentials
* can never be verified. */
g_credentials =
credentials_load(opts.password_file, opts.early_input_file, cli_err, sizeof(cli_err));
if (!g_credentials) {
server_cli_options_free(&opts);
fprintf(stderr, "Error: %s\n", cli_err);
return 1;
}
bool credential_source_given = opts.password_file != NULL || opts.early_input_file != NULL;
for (int i = 0; i < g_daemon_conf->module_count; i++) {
const DaemonModule* module = &g_daemon_conf->modules[i];
if (module->auth_user_count == 0)
continue;
if (!credential_source_given) {
fprintf(stderr,
"Error: module '%s' declares 'auth users' but no credential store was given "
"(--password-file or --early-input); refusing to start (fail closed)\n",
module->name);
server_cli_options_free(&opts);
return 1;
}
if (credentials_store_size(g_credentials) == 0) {
fprintf(stderr,
"Error: module '%s' declares 'auth users' but the credential store is empty; "
"refusing to start (fail closed)\n",
module->name);
server_cli_options_free(&opts);
return 1;
}
for (int j = 0; j < module->auth_user_count; j++) {
if (!credentials_store_has(g_credentials, module->auth_users[j]))
log_message(LOG_LEVEL_WARNING,
"daemon module '%s': auth user '%s' has no credential store entry; that "
"user can never authenticate",
module->name, module->auth_users[j]);
}
}
/* Shared cross-process registry for the per-module / per-source caps and
* the auth lockout. Created HERE in the parent before any accept-loop
* fork; every connection child inherits the mapping. A failure degrades to
* "registry disabled" (the global cap and host ACLs still apply) rather
* than refusing to start. */
g_daemon_limits = daemon_limits_create((int)g_daemon_conf->global.max_connections,
g_daemon_conf->module_count,
g_daemon_conf->global.max_connections_per_host,
g_daemon_conf->global.auth_lockout_threshold,
g_daemon_conf->global.auth_lockout_duration_sec);
if (!g_daemon_limits)
log_message(LOG_LEVEL_WARNING,
"daemon: could not allocate the shared connection registry; per-module / "
"per-host caps and the cross-process auth lockout are disabled (the global "
"'max connections' cap and host ACLs still apply)");
} else {
if (!configure_authorization(opts.destination_root)) {
char* escaped = output_escape(opts.destination_root, false);
fprintf(stderr, "Error: invalid destination root '%s'\n",
escaped ? escaped : "<allocation failed>");
free(escaped);
server_cli_options_free(&opts);
return 1;
}
}
ServerBindOptions bind_opts;
bind_opts.bind_address = bind_address;
bind_opts.family = bind_family;
g_server = server_create_ex(port, &bind_opts);
if (!g_server) {
log_message(LOG_LEVEL_ERROR, "Failed to create server");
release_authorization();
exit_code = 1;
goto out;
}
if (g_daemon_conf)
server_set_max_connections(g_server, (unsigned int)g_daemon_conf->global.max_connections);
if (g_daemon_limits)
server_set_limit_registry(g_server, g_daemon_limits);
if (opts.use_tls) {
if (!opts.tls_cert || !opts.tls_key || !opts.tls_ca || !opts.client_cn) {
fprintf(stderr, "Error: --tls requires --cert, --key, --ca, and --client-cn\n");
server_delete(&g_server);
release_authorization();
exit_code = 1;
goto out;
}
tls_global_init();
if (!server_create_tls(g_server, opts.tls_cert, opts.tls_key, opts.tls_ca)) {
log_message(LOG_LEVEL_ERROR, "Failed to set up TLS");
server_delete(&g_server);
release_authorization();
exit_code = 1;
goto out;
}
}
/* Detach after the listening socket (and TLS context) exist so the
* background daemon inherits a fully-bound listener. --no-detach runs in
* the foreground, which is how tests drive the daemon. */
if (opts.daemon_mode && !opts.no_detach) {
if (!daemonize()) {
log_message(LOG_LEVEL_ERROR, "Failed to daemonize");
server_delete(&g_server);
release_authorization();
exit_code = 1;
goto out;
}
}
if (opts.use_tls)
server_listen_tls(g_server, handler);
else
server_listen(g_server, handler);
server_delete(&g_server);
release_authorization();
out:
daemon_limits_destroy(g_daemon_limits);
g_daemon_limits = NULL;
daemon_conf_free(g_daemon_conf);
g_daemon_conf = NULL;
credentials_free(g_credentials);
g_credentials = NULL;
server_cli_options_free(&opts);
return exit_code;
} }
#endif #endif
+258 -186
View File
@@ -224,6 +224,235 @@ typedef struct {
void* observer_context; /* DELETE mode */ void* observer_context; /* DELETE mode */
} DeleteWalkState; } DeleteWalkState;
/* The per-walk invariants threaded unchanged through every recursive descent:
the keep/synchronized-dir indexes, the destination mode and the protection
rules. Bundling them keeps the recursive helpers below to a handful of
positional arguments. */
typedef struct {
const PathIndex* keep;
const PathIndex* dirs;
DeleteWalkState* state;
const DeleteSkipEntry* skips;
int skip_count;
const DeleteProtectRules* protect;
} DeleteWalkContext;
/* Duplicate `path` with rsync's trailing-slash convention, used to report a
removed (or would-be-removed) directory. Returns NULL on allocation
failure. */
static char* with_trailing_slash(const char* path) {
size_t len = strlen(path);
char* copy = malloc(len + 2);
if (!copy)
return NULL;
memcpy(copy, path, len);
copy[len] = '/';
copy[len + 1] = '\0';
return copy;
}
/* Forward declaration: the ordered passes below recurse through the driver. */
static bool delete_walk_fd(int dirfd, const char* rel_path, const DeleteWalkContext* ctx,
bool parent_deletable, bool* all_removed);
/* Descend into the child directory `name` of `dirfd`, walking it as part of the
current operation. Returns false on a genuine open/walk failure; on success
*child_all_removed reports whether the child removed everything it held (so
the caller may rmdir it). */
static bool delete_walk_child(int dirfd, const char* name, const char* child_rel,
const DeleteWalkContext* ctx, bool deletable,
bool* child_all_removed) {
*child_all_removed = false;
int childfd = openat(dirfd, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (childfd < 0)
return errno == ENOENT;
bool ok = delete_walk_fd(childfd, child_rel, ctx, deletable, child_all_removed);
close(childfd);
return ok;
}
/* Classify every entry up front (the verdict does not depend on processing
order) so the ordered passes below can act on it. Sets shielded[]/is_extra[]
and reports through *local_survives whether anything in this directory stays
in place. Returns false on a path-construction failure. */
static bool delete_walk_classify(const char* rel_path, const DeleteDirEntry* entries, size_t count,
const DeleteWalkContext* ctx, bool deletable, bool at_root,
bool* shielded, bool* is_extra, bool* local_survives) {
bool ok = true;
for (size_t i = 0; i < count; i++) {
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
ok = false;
continue;
}
/* A --delay-updates run keeps its staging directory as a direct child of
the receive root, and basis-dir snapshots live below it too. Their
contents are not manifest entries, so descending into them would delete
every staged / basis file as an "extra". Only the staging name (a
top-level-only prefix) and the basis prefixes are protected: a nested
destination directory that happens to be called .fastsync-stage is
ordinary content. */
if (path_under_skip_prefix(child_rel, at_root, ctx->skips, ctx->skip_count)) {
shielded[i] = true;
*local_survives = true;
} else if (delete_protect_verdict(ctx->protect, child_rel, entries[i].name,
entries[i].is_dir) == FILTER_ACTION_PROTECT) {
/* A first-match protect rule shields the extra; for a directory the whole
subtree is shielded (rsync prunes an excluded directory), so do not
descend. */
shielded[i] = true;
*local_survives = true;
} else if (entries[i].is_dir) {
bool child_synced = ctx->dirs && path_index_contains(ctx->dirs, child_rel);
is_extra[i] = deletable && !child_synced && !keep_is_dir(ctx->keep, child_rel);
if (!is_extra[i])
*local_survives = true;
} else {
is_extra[i] = deletable && !keep_is_file(ctx->keep, child_rel);
if (!is_extra[i])
*local_survives = true;
}
free(child_rel);
}
return ok;
}
/* Pass 1: extraneous subdirectories, descending. Recurses into each and, when
the child removed everything it held, records or removes it and charges the
budget. */
static bool delete_walk_extra_dirs(int dirfd, const char* rel_path, const DeleteDirEntry* entries,
size_t dir_count, const DeleteWalkContext* ctx, bool deletable,
const bool* is_extra, bool* local_survives) {
bool ok = true;
for (size_t i = 0; i < dir_count; i++) {
if (!is_extra[i])
continue;
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
ok = false;
continue;
}
bool child_all_removed = false;
if (!delete_walk_child(dirfd, entries[i].name, child_rel, ctx, deletable, &child_all_removed))
ok = false;
if (child_all_removed && deletable) {
if (ctx->state->mode == DELETE_WALK_MODE_LIST) {
/* Record the directory with rsync's trailing slash. */
char* copy = with_trailing_slash(child_rel);
if (!copy) {
ok = false;
} else if (!array_list_add(ctx->state->out, copy)) {
free(copy);
ok = false;
} else {
(*ctx->state->recorded)++;
}
} else if (ctx->state->budget->deleted >= ctx->state->budget->max_delete) {
ctx->state->budget->limit_hit = true;
ctx->state->budget->skipped++;
*local_survives = true;
} else if (unlinkat(dirfd, entries[i].name, AT_REMOVEDIR) != 0) {
/* ENOENT: already gone (fine). ENOTEMPTY/EEXIST: the directory still
holds entries the walker leaves in place (a protected excluded
prefix, a kept file the manifest protects, a symlink); rsync leaves
such a directory behind, so this is not an error. Only genuine I/O
failures abort the deletion. */
if (errno != ENOENT && errno != ENOTEMPTY && errno != EEXIST)
ok = false;
*local_survives = true;
} else {
ctx->state->budget->deleted++;
/* rsync reports a removed directory with a trailing slash. */
if (ctx->state->observer) {
char* with_slash = with_trailing_slash(child_rel);
if (with_slash) {
ctx->state->observer(ctx->state->observer_context, with_slash, DELETE_ENTRY_DIR);
free(with_slash);
} else {
ctx->state->observer(ctx->state->observer_context, child_rel, DELETE_ENTRY_DIR);
}
}
}
} else {
*local_survives = true;
}
free(child_rel);
}
return ok;
}
/* Pass 2: extraneous files, descending. */
static bool delete_walk_extra_files(int dirfd, const char* rel_path, const DeleteDirEntry* entries,
size_t dir_count, size_t count, const DeleteWalkContext* ctx,
const bool* is_extra, bool* local_survives) {
bool ok = true;
for (size_t i = dir_count; i < count; i++) {
if (!is_extra[i])
continue;
if (ctx->state->mode == DELETE_WALK_MODE_LIST) {
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
ok = false;
continue;
}
char* copy = str_dup(child_rel);
if (!copy || !array_list_add(ctx->state->out, copy)) {
free(copy);
ok = false;
} else {
(*ctx->state->recorded)++;
}
free(child_rel);
} else if (ctx->state->budget->deleted >= ctx->state->budget->max_delete) {
ctx->state->budget->limit_hit = true;
ctx->state->budget->skipped++;
*local_survives = true;
} else if (unlinkat(dirfd, entries[i].name, 0) != 0) {
if (errno != ENOENT)
ok = false;
*local_survives = true;
} else {
ctx->state->budget->deleted++;
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (child_rel) {
if (ctx->state->observer)
ctx->state->observer(ctx->state->observer_context, child_rel,
delete_entry_type_of_mode(entries[i].mode));
char* escaped_path = output_escape(child_rel, log_get_8_bit_output());
fprintf(stderr, " Deleted: %s\n", escaped_path ? escaped_path : "<allocation failed>");
free(escaped_path);
}
free(child_rel);
}
}
return ok;
}
/* Pass 3: kept subdirectories, ascending (rsync descends into these only after
the parent's own extras have been handled). */
static bool delete_walk_kept_dirs(int dirfd, const char* rel_path, const DeleteDirEntry* entries,
size_t dir_count, const DeleteWalkContext* ctx, bool deletable,
const bool* is_extra, const bool* shielded,
bool* local_survives) {
bool ok = true;
for (size_t i = dir_count; i-- > 0;) {
if (is_extra[i] || shielded[i])
continue;
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
ok = false;
continue;
}
bool child_all_removed = false;
if (!delete_walk_child(dirfd, entries[i].name, child_rel, ctx, deletable, &child_all_removed))
ok = false;
/* A kept/synchronized directory is never removed. */
*local_survives = true;
free(child_rel);
}
return ok;
}
/* Remove the extras directly inside the directory open on `dirfd` (DELETE mode) /* Remove the extras directly inside the directory open on `dirfd` (DELETE mode)
or record the paths that WOULD be removed (LIST mode), recursing into every or record the paths that WOULD be removed (LIST mode), recursing into every
child directory so kept content below a synchronized prefix is reached. child directory so kept content below a synchronized prefix is reached.
@@ -238,11 +467,8 @@ typedef struct {
descending name order, then extraneous files, then kept subdirectories in descending name order, then extraneous files, then kept subdirectories in
ascending order) rather than readdir() order, so `--max-delete` leaves the ascending order) rather than readdir() order, so `--max-delete` leaves the
same survivors and the `--info=del`/dry-run line order matches rsync. */ same survivors and the `--info=del`/dry-run line order matches rsync. */
static bool delete_walk_fd(int dirfd, const char* rel_path, const PathIndex* keep, static bool delete_walk_fd(int dirfd, const char* rel_path, const DeleteWalkContext* ctx,
const PathIndex* dirs, DeleteWalkState* state, bool parent_deletable, bool* all_removed) {
const DeleteSkipEntry* skips, int skip_count,
const DeleteProtectRules* protect, bool parent_deletable,
bool* all_removed) {
DeleteDirEntry* entries = NULL; DeleteDirEntry* entries = NULL;
size_t count = 0; size_t count = 0;
bool collect_ok = true; bool collect_ok = true;
@@ -261,7 +487,7 @@ static bool delete_walk_fd(int dirfd, const char* rel_path, const PathIndex* kee
/* A directory is deletable when it or ANY ancestor is synchronized; the /* A directory is deletable when it or ANY ancestor is synchronized; the
`parent_deletable` flag carries that down the recursion so dest-only `parent_deletable` flag carries that down the recursion so dest-only
directories below a synchronized root are removed wholesale. */ directories below a synchronized root are removed wholesale. */
bool deletable = parent_deletable || is_synced_dir(dirs, rel_path); bool deletable = parent_deletable || is_synced_dir(ctx->dirs, rel_path);
bool at_root = rel_path[0] == '\0'; bool at_root = rel_path[0] == '\0';
/* Reproduce rsync's traversal order: extraneous subdirectories in descending /* Reproduce rsync's traversal order: extraneous subdirectories in descending
@@ -274,182 +500,18 @@ static bool delete_walk_fd(int dirfd, const char* rel_path, const PathIndex* kee
while (dir_count < count && entries[dir_count].is_dir) while (dir_count < count && entries[dir_count].is_dir)
dir_count++; dir_count++;
/* Classify every entry up front (the verdict does not depend on processing if (!delete_walk_classify(rel_path, entries, count, ctx, deletable, at_root, shielded, is_extra,
order) so the ordered passes below can act on it. */ &local_survives))
for (size_t i = 0; i < count; i++) { operation_ok = false;
char* child_rel = path_cat((char*)rel_path, entries[i].name); if (!delete_walk_extra_dirs(dirfd, rel_path, entries, dir_count, ctx, deletable, is_extra,
if (!child_rel) { &local_survives))
operation_ok = false; operation_ok = false;
continue; if (!delete_walk_extra_files(dirfd, rel_path, entries, dir_count, count, ctx, is_extra,
} &local_survives))
/* A --delay-updates run keeps its staging directory as a direct child of operation_ok = false;
the receive root, and basis-dir snapshots live below it too. Their if (!delete_walk_kept_dirs(dirfd, rel_path, entries, dir_count, ctx, deletable, is_extra,
contents are not manifest entries, so descending into them would delete shielded, &local_survives))
every staged / basis file as an "extra". Only the staging name (a operation_ok = false;
top-level-only prefix) and the basis prefixes are protected: a nested
destination directory that happens to be called .fastsync-stage is
ordinary content. */
if (path_under_skip_prefix(child_rel, at_root, skips, skip_count)) {
shielded[i] = true;
local_survives = true;
} else if (delete_protect_verdict(protect, child_rel, entries[i].name, entries[i].is_dir) ==
FILTER_ACTION_PROTECT) {
/* A first-match protect rule shields the extra; for a directory the whole
subtree is shielded (rsync prunes an excluded directory), so do not
descend. */
shielded[i] = true;
local_survives = true;
} else if (entries[i].is_dir) {
bool child_synced = dirs && path_index_contains(dirs, child_rel);
is_extra[i] = deletable && !child_synced && !keep_is_dir(keep, child_rel);
if (!is_extra[i])
local_survives = true;
} else {
is_extra[i] = deletable && !keep_is_file(keep, child_rel);
if (!is_extra[i])
local_survives = true;
}
free(child_rel);
}
/* Pass 1: extraneous subdirectories, descending. */
for (size_t i = 0; i < dir_count; i++) {
if (!is_extra[i])
continue;
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
operation_ok = false;
continue;
}
int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
bool child_all_removed = false;
if (childfd >= 0) {
if (!delete_walk_fd(childfd, child_rel, keep, dirs, state, skips, skip_count, protect,
deletable, &child_all_removed))
operation_ok = false;
close(childfd);
} else if (errno != ENOENT) {
operation_ok = false;
}
if (child_all_removed && deletable) {
if (state->mode == DELETE_WALK_MODE_LIST) {
/* Record the directory with rsync's trailing slash. */
size_t len = strlen(child_rel);
char* copy = malloc(len + 2);
if (!copy) {
operation_ok = false;
} else {
memcpy(copy, child_rel, len);
copy[len] = '/';
copy[len + 1] = '\0';
if (!array_list_add(state->out, copy)) {
free(copy);
operation_ok = false;
} else {
(*state->recorded)++;
}
}
} else if (state->budget->deleted >= state->budget->max_delete) {
state->budget->limit_hit = true;
state->budget->skipped++;
local_survives = true;
} else if (unlinkat(dirfd, entries[i].name, AT_REMOVEDIR) != 0) {
/* ENOENT: already gone (fine). ENOTEMPTY/EEXIST: the directory still
holds entries the walker leaves in place (a protected excluded
prefix, a kept file the manifest protects, a symlink); rsync leaves
such a directory behind, so this is not an error. Only genuine I/O
failures abort the deletion. */
if (errno != ENOENT && errno != ENOTEMPTY && errno != EEXIST)
operation_ok = false;
local_survives = true;
} else {
state->budget->deleted++;
/* rsync reports a removed directory with a trailing slash. */
if (state->observer) {
size_t len = strlen(child_rel);
char* with_slash = malloc(len + 2);
if (with_slash) {
memcpy(with_slash, child_rel, len);
with_slash[len] = '/';
with_slash[len + 1] = '\0';
state->observer(state->observer_context, with_slash, DELETE_ENTRY_DIR);
free(with_slash);
} else {
state->observer(state->observer_context, child_rel, DELETE_ENTRY_DIR);
}
}
}
} else {
local_survives = true;
}
free(child_rel);
}
/* Pass 2: extraneous files, descending. */
for (size_t i = dir_count; i < count; i++) {
if (!is_extra[i])
continue;
if (state->mode == DELETE_WALK_MODE_LIST) {
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
operation_ok = false;
continue;
}
char* copy = str_dup(child_rel);
if (!copy || !array_list_add(state->out, copy)) {
free(copy);
operation_ok = false;
} else {
(*state->recorded)++;
}
free(child_rel);
} else if (state->budget->deleted >= state->budget->max_delete) {
state->budget->limit_hit = true;
state->budget->skipped++;
local_survives = true;
} else if (unlinkat(dirfd, entries[i].name, 0) != 0) {
if (errno != ENOENT)
operation_ok = false;
local_survives = true;
} else {
state->budget->deleted++;
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (child_rel) {
if (state->observer)
state->observer(state->observer_context, child_rel,
delete_entry_type_of_mode(entries[i].mode));
char* escaped_path = output_escape(child_rel, log_get_8_bit_output());
fprintf(stderr, " Deleted: %s\n", escaped_path ? escaped_path : "<allocation failed>");
free(escaped_path);
}
free(child_rel);
}
}
/* Pass 3: kept subdirectories, ascending (rsync descends into these only
after the parent's own extras have been handled). */
for (size_t i = dir_count; i-- > 0;) {
if (is_extra[i] || shielded[i])
continue;
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
operation_ok = false;
continue;
}
int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
bool child_all_removed = false;
if (childfd >= 0) {
if (!delete_walk_fd(childfd, child_rel, keep, dirs, state, skips, skip_count, protect,
deletable, &child_all_removed))
operation_ok = false;
close(childfd);
} else if (errno != ENOENT) {
operation_ok = false;
}
/* A kept/synchronized directory is never removed. */
local_survives = true;
free(child_rel);
}
free(shielded); free(shielded);
free(is_extra); free(is_extra);
@@ -504,8 +566,13 @@ bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
.recorded = &recorded, .recorded = &recorded,
.observer = NULL, .observer = NULL,
.observer_context = NULL}; .observer_context = NULL};
bool ok = delete_walk_fd(rootfd, "", &keep, have_dirs ? &dirs : NULL, &state, skips, skip_count, DeleteWalkContext ctx = {.keep = &keep,
protect, false, &all_removed); .dirs = have_dirs ? &dirs : NULL,
.state = &state,
.skips = skips,
.skip_count = skip_count,
.protect = protect};
bool ok = delete_walk_fd(rootfd, "", &ctx, false, &all_removed);
if (close(rootfd) != 0) if (close(rootfd) != 0)
ok = false; ok = false;
path_index_free(&keep); path_index_free(&keep);
@@ -557,8 +624,13 @@ DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const Arr
.recorded = NULL, .recorded = NULL,
.observer = observer, .observer = observer,
.observer_context = observer_context}; .observer_context = observer_context};
bool ok = delete_walk_fd(rootfd, "", &keep, have_dirs ? &dirs : NULL, &state, skips, skip_count, DeleteWalkContext ctx = {.keep = &keep,
protect, false, &all_removed); .dirs = have_dirs ? &dirs : NULL,
.state = &state,
.skips = skips,
.skip_count = skip_count,
.protect = protect};
bool ok = delete_walk_fd(rootfd, "", &ctx, false, &all_removed);
if (close(rootfd) != 0) if (close(rootfd) != 0)
ok = false; ok = false;
path_index_free(&keep); path_index_free(&keep);
+162 -135
View File
@@ -227,6 +227,166 @@ static void prefixed_delete_observer(void* context, const char* rel, DeleteEntry
--max-delete budget: once it is exhausted the remaining requests are skipped --max-delete budget: once it is exhausted the remaining requests are skipped
and counted. Returns false only on a genuine error (a confinement failure on and counted. Returns false only on a genuine error (a confinement failure on
a validated path or an I/O error), which fails the run. */ a validated path or an I/O error), which fails the run. */
/* How one missing-args request leaves the driver loop. The original walker
`continue`s past an invalid/protected/absent/budget-skipped request (without
breaking) but stops after a request that ran to completion while an error is
pending; NEXT/STOP preserve that control flow exactly. */
typedef enum { MISSING_ARG_NEXT, MISSING_ARG_STOP } MissingArgStep;
/* Remove a NON-empty missing-args directory recursively (--delete/--force in
effect): walk its contents through the budgeted extras walker so every removed
file/dir counts toward --max-delete (rsync parity), then remove the now-empty
directory itself, which costs one more budget unit. A run that hits the cap
leaves the remaining entries in place. The observer is wrapped so the nested
walk reports receive-root-relative paths. Sets the *removed and *ok outputs. */
static void delete_nonempty_missing_dir(const char* full, const char* rel,
DeleteBudgetState* budget, DeletePathObserver observer,
void* observer_context, bool* removed, bool* ok) {
ArrayList* no_keeps = array_list_create(free);
/* Never let an accounting slip (deleted > max_delete) underflow the remaining
budget into SIZE_MAX, which would grant unlimited deletions. */
size_t remaining =
budget->deleted >= budget->max_delete ? 0 : budget->max_delete - budget->deleted;
size_t contents_deleted = 0;
size_t contents_skipped = 0;
PrefixedDeleteObserver nested = {observer, observer_context, rel};
DeleteWalkResult walk =
no_keeps ? delete_extras_limited_observed(full, no_keeps, NULL, remaining, NULL, 0, NULL,
&contents_deleted, &contents_skipped,
observer ? prefixed_delete_observer : NULL,
observer ? &nested : NULL)
: DELETE_WALK_ERROR;
if (no_keeps)
array_list_delete(no_keeps);
budget->deleted += contents_deleted;
budget->skipped += contents_skipped;
if (walk == DELETE_WALK_LIMIT_REACHED) {
budget->limit_hit = true;
} else if (walk != DELETE_WALK_OK) {
*ok = false;
} else if (budget->deleted >= budget->max_delete) {
budget->limit_hit = true;
budget->skipped++;
} else if (file_remove_tree_secure(full)) {
/* The shared `if (removed)` tail charges this directory exactly once;
counting it here too would consume two budget units. */
*removed = true;
} else {
*ok = false;
}
}
/* Remove one missing-args destination mirror. `skips` holds the receiver
artifacts (staging directory, basis snapshots) that stay protected. Returns
MISSING_ARG_STOP when the driver loop must stop (a completed removal left a
genuine error pending) and MISSING_ARG_NEXT otherwise; *ok accumulates the
overall success across the whole run. */
static MissingArgStep delete_one_missing_arg(const Config* config, const char* rel,
const DeleteSkipSet* skips, DeleteBudgetState* budget,
DeletePathObserver observer, void* observer_context,
bool* ok) {
if (!rel || *rel == '\0' || *rel == '/' || has_path_traversal(rel)) {
/* Defensive only: receive_manifest_entries already validated every
section identically, so a controlled peer never reaches this branch. */
log_message(LOG_LEVEL_ERROR, "invalid missing-args delete path");
*ok = false;
return MISSING_ARG_NEXT;
}
bool at_root = strchr(rel, '/') == NULL;
if (path_under_skip_prefix(rel, at_root, skips->entries, skips->count)) {
char* escaped = output_escape(rel, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING,
"missing-args path '%s' is protected (staging directory or basis snapshot); "
"not deleting",
escaped ? escaped : "<allocation failed>");
free(escaped);
return MISSING_ARG_NEXT;
}
char* full = path_cat(config->receive_root_directory, rel);
if (!full) {
*ok = false;
return MISSING_ARG_NEXT;
}
char* leaf = NULL;
int parent_fd = file_open_secure_parent(full, &leaf, false);
if (parent_fd < 0) {
/* The mirror's parent directory may itself not exist on the destination
(a deeper missing entry whose leading directories were never created).
That is a no-op -- there is nothing to delete -- matching
file_remove_tree_secure's absent-path handling; only a genuine I/O
error (EACCES, a symlink loop, ...) fails the run. */
bool absent = errno == ENOENT || errno == ENOTDIR;
free(full);
free(leaf);
if (!absent)
*ok = false;
return MISSING_ARG_NEXT;
}
struct stat st;
if (fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) != 0) {
/* Already absent: nothing to delete (a no-op, not a deletion). */
if (errno != ENOENT)
*ok = false;
close(parent_fd);
free(leaf);
free(full);
return MISSING_ARG_NEXT;
}
/* An entry that exists is one deletion: skip it (and count it) when the
shared --max-delete budget is already exhausted. */
if (budget->deleted >= budget->max_delete) {
budget->limit_hit = true;
budget->skipped++;
close(parent_fd);
free(leaf);
free(full);
return MISSING_ARG_NEXT;
}
bool removed = false;
if (S_ISDIR(st.st_mode)) {
if (unlinkat(parent_fd, leaf, AT_REMOVEDIR) == 0) {
removed = true;
} else if (errno == ENOTEMPTY || errno == EEXIST) {
close(parent_fd);
parent_fd = -1;
free(leaf);
leaf = NULL;
if (config->use_delete || config->force_delete) {
delete_nonempty_missing_dir(full, rel, budget, observer, observer_context, &removed, ok);
} else {
char* escaped = output_escape(rel, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING,
"missing-args destination '%s' is a non-empty directory; use --force or "
"--delete to remove it",
escaped ? escaped : "<allocation failed>");
free(escaped);
}
} else if (errno != ENOENT) {
*ok = false;
}
} else {
if (unlinkat(parent_fd, leaf, 0) == 0) {
removed = true;
} else if (errno != ENOENT) {
*ok = false;
}
}
if (removed) {
budget->deleted++;
if (observer)
observer(observer_context, rel, delete_entry_type_of_mode(st.st_mode));
char* escaped = output_escape(rel, log_get_8_bit_output());
fprintf(stderr, " Deleted: %s\n", escaped ? escaped : "<allocation failed>");
free(escaped);
}
if (parent_fd >= 0)
close(parent_fd);
free(leaf);
free(full);
return *ok ? MISSING_ARG_NEXT : MISSING_ARG_STOP;
}
static bool delete_missing_args_budgeted_observed(const Config* config, static bool delete_missing_args_budgeted_observed(const Config* config,
const DeleteManifest* manifest, const DeleteManifest* manifest,
DeleteBudgetState* budget, DeleteBudgetState* budget,
@@ -246,141 +406,8 @@ static bool delete_missing_args_budgeted_observed(const Config* config,
bool ok = true; bool ok = true;
for (int i = 0; i < manifest->missing->size; i++) { for (int i = 0; i < manifest->missing->size; i++) {
const char* rel = (const char*)manifest->missing->items[i]; const char* rel = (const char*)manifest->missing->items[i];
if (!rel || *rel == '\0' || *rel == '/' || has_path_traversal(rel)) { if (delete_one_missing_arg(config, rel, &skips, budget, observer, observer_context, &ok) ==
/* Defensive only: receive_manifest_entries already validated every MISSING_ARG_STOP)
section identically, so a controlled peer never reaches this branch. */
log_message(LOG_LEVEL_ERROR, "invalid missing-args delete path");
ok = false;
continue;
}
bool at_root = strchr(rel, '/') == NULL;
if (path_under_skip_prefix(rel, at_root, skips.entries, skips.count)) {
char* escaped = output_escape(rel, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING,
"missing-args path '%s' is protected (staging directory or basis snapshot); "
"not deleting",
escaped ? escaped : "<allocation failed>");
free(escaped);
continue;
}
char* full = path_cat(config->receive_root_directory, rel);
if (!full) {
ok = false;
continue;
}
char* leaf = NULL;
int parent_fd = file_open_secure_parent(full, &leaf, false);
if (parent_fd < 0) {
/* The mirror's parent directory may itself not exist on the destination
(a deeper missing entry whose leading directories were never created).
That is a no-op -- there is nothing to delete -- matching
file_remove_tree_secure's absent-path handling; only a genuine I/O
error (EACCES, a symlink loop, ...) fails the run. */
bool absent = errno == ENOENT || errno == ENOTDIR;
free(full);
free(leaf);
if (!absent)
ok = false;
continue;
}
struct stat st;
if (fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) != 0) {
/* Already absent: nothing to delete (a no-op, not a deletion). */
if (errno != ENOENT)
ok = false;
close(parent_fd);
free(leaf);
free(full);
continue;
}
/* An entry that exists is one deletion: skip it (and count it) when the
shared --max-delete budget is already exhausted. */
if (budget->deleted >= budget->max_delete) {
budget->limit_hit = true;
budget->skipped++;
close(parent_fd);
free(leaf);
free(full);
continue;
}
bool removed = false;
if (S_ISDIR(st.st_mode)) {
if (unlinkat(parent_fd, leaf, AT_REMOVEDIR) == 0) {
removed = true;
} else if (errno == ENOTEMPTY || errno == EEXIST) {
close(parent_fd);
parent_fd = -1;
free(leaf);
leaf = NULL;
if (config->use_delete || config->force_delete) {
/* Remove the contents entry-by-entry through the budgeted extras
walker so every deleted file/dir counts toward --max-delete (rsync
parity); the now-empty directory itself costs one more. A run that
hits the cap leaves the remaining entries in place. */
ArrayList* no_keeps = array_list_create(free);
/* Never let an accounting slip (deleted > max_delete) underflow the
remaining budget into SIZE_MAX, which would grant unlimited
deletions. */
size_t remaining =
budget->deleted >= budget->max_delete ? 0 : budget->max_delete - budget->deleted;
size_t contents_deleted = 0;
size_t contents_skipped = 0;
PrefixedDeleteObserver nested = {observer, observer_context, rel};
DeleteWalkResult walk =
no_keeps ? delete_extras_limited_observed(full, no_keeps, NULL, remaining, NULL, 0,
NULL, &contents_deleted, &contents_skipped,
observer ? prefixed_delete_observer : NULL,
observer ? &nested : NULL)
: DELETE_WALK_ERROR;
if (no_keeps)
array_list_delete(no_keeps);
budget->deleted += contents_deleted;
budget->skipped += contents_skipped;
if (walk == DELETE_WALK_LIMIT_REACHED) {
budget->limit_hit = true;
} else if (walk != DELETE_WALK_OK) {
ok = false;
} else if (budget->deleted >= budget->max_delete) {
budget->limit_hit = true;
budget->skipped++;
} else if (file_remove_tree_secure(full)) {
/* The shared `if (removed)` tail charges this directory exactly
once; counting it here too would consume two budget units. */
removed = true;
} else {
ok = false;
}
} else {
char* escaped = output_escape(rel, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING,
"missing-args destination '%s' is a non-empty directory; use --force or "
"--delete to remove it",
escaped ? escaped : "<allocation failed>");
free(escaped);
}
} else if (errno != ENOENT) {
ok = false;
}
} else {
if (unlinkat(parent_fd, leaf, 0) == 0) {
removed = true;
} else if (errno != ENOENT) {
ok = false;
}
}
if (removed) {
budget->deleted++;
if (observer)
observer(observer_context, rel, delete_entry_type_of_mode(st.st_mode));
char* escaped = output_escape(rel, log_get_8_bit_output());
fprintf(stderr, " Deleted: %s\n", escaped ? escaped : "<allocation failed>");
free(escaped);
}
if (parent_fd >= 0)
close(parent_fd);
free(leaf);
free(full);
if (!ok)
break; break;
} }
delete_skips_free(&skips); delete_skips_free(&skips);
+64 -34
View File
@@ -102,12 +102,14 @@ bool dir_metadata_should_capture(const Config* config) {
/* Directory metadata is captured when a directory attribute is actually /* Directory metadata is captured when a directory attribute is actually
* requested: -p/--perms (directory modes), -t/--times (directory mtimes, * requested: -p/--perms (directory modes), -t/--times (directory mtimes,
* unless -O/--omit-dir-times suppresses them), -o/-g (directory ownership), * unless -O/--omit-dir-times suppresses them), -o/-g (directory ownership),
* or -X/-A (directory xattrs/ACLs). --atimes/-U alone does not pull * -X/-A (directory xattrs/ACLs), or --fake-super (whose reserved %stat record
* directory metadata (matching the original dir-time bundle). */ * is written on the directory itself, so its metadata must travel).
* --atimes/-U alone does not pull directory metadata (matching the original
* dir-time bundle). */
return config && config->use_metadata && return config && config->use_metadata &&
(config->preserve_perms || (config->preserve_times && !config->omit_dir_times) || (config->preserve_perms || (config->preserve_times && !config->omit_dir_times) ||
config->preserve_owner || config->preserve_group || config->preserve_xattrs || config->preserve_owner || config->preserve_group || config->preserve_xattrs ||
config->preserve_acls); config->preserve_acls || config->fake_super);
} }
void dir_time_list_init(DirTimeList* list) { void dir_time_list_init(DirTimeList* list) {
@@ -205,12 +207,19 @@ void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory
bool apply_times = config->preserve_times && !config->omit_dir_times; bool apply_times = config->preserve_times && !config->omit_dir_times;
bool apply_mode = config->preserve_perms; bool apply_mode = config->preserve_perms;
bool apply_xattrs = config->use_xattrs; bool apply_xattrs = config->use_xattrs;
bool apply_fake_super = config->fake_super;
/* Ownership is applied through the active identity snapshot (which no-ops /* Ownership is applied through the active identity snapshot (which no-ops
* unless an ownership request is active), and xattrs only when -X/-A was * unless an ownership request is active), xattrs only when -X/-A was
* negotiated. Times/mode keep their own per-attribute gates. */ * negotiated, and the --fake-super record whenever the flag is active.
bool have_any = apply_times || apply_mode || apply_xattrs || identity_active_enabled(); * Times/mode keep their own per-attribute gates. */
bool have_any =
apply_times || apply_mode || apply_xattrs || apply_fake_super || identity_active_enabled();
if (!have_any) if (!have_any)
return; return;
/* Built once: the --fake-super replay uses it to apply only the recorded
* permission bits (the special bits stay in the record, exactly like the
* regular-file fake-super receiver). */
FileAttrPolicy policy = file_attr_policy_from_config(config);
for (size_t i = 0; i < list->count; i++) { for (size_t i = 0; i < list->count; i++) {
char* dir_path = path_cat(root_directory, list->paths[i]); char* dir_path = path_cat(root_directory, list->paths[i]);
if (!dir_path) if (!dir_path)
@@ -260,38 +269,59 @@ void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory
free(escaped_path); free(escaped_path);
} }
} }
if (apply_mode) { /* The final directory mode (after any --chmod) is computed once so the
mode_t dir_mode = list->entries[i].mode; --fake-super record can carry it even when the on-disk replay is
bool mode_ready = true; restricted to the permission bits below. */
if (config->chmod_spec && *config->chmod_spec && mode_t dir_mode = list->entries[i].mode;
!chmod_apply(dir_mode, config->chmod_spec, &dir_mode)) { bool mode_ready = true;
if (apply_mode && 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 : "<allocation failed>");
free(escaped_path);
mode_ready = false;
}
/* Under --fake-super the normal fchmod below still applies the mode, but
the fake-super replay that follows narrows the on-disk result to the
recorded permission bits (the full mode, including setuid/setgid/sticky,
lives only in the record). Keeping the normal fchmod first means a
filesystem without xattr support still gets the directory mode rather than
silently losing it. */
if (apply_mode && 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()); char* escaped_path = output_escape(dir_path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING, "Failed to apply --chmod to directory %s", log_message(LOG_LEVEL_WARNING, "Failed to open directory %s to set its mode: %s",
escaped_path ? escaped_path : "<allocation failed>"); escaped_path ? escaped_path : "<allocation failed>", 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 : "<allocation failed>", strerror(errno));
free(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 : "<allocation failed>", 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 : "<allocation failed>", strerror(errno));
free(escaped_path);
}
} }
} }
/* --fake-super: park the directory's full stat (rsync 3.4.1's exact
grammar) on the directory ITSELF, then replay only the recorded
permission bits fd-relative. The special bits live only in the record
and the recorded ownership is never real-chowned: the resolved ids are
stored for a later privileged restore, exactly like the file path. Runs
before the xattr apply so a mode change cannot clobber the ACL mask. */
if (apply_fake_super && dir_fd >= 0) {
uint32_t store_uid = 0;
uint32_t store_gid = 0;
identity_resolve_storage_ids((int32_t)list->entries[i].uid, (int32_t)list->entries[i].gid,
&store_uid, &store_gid);
fake_super_store_fd(dir_fd, store_uid, store_gid, (uint32_t)dir_mode, 0, 0);
fake_super_restore_fd(dir_fd, policy);
}
/* xattrs/ACLs last: a mode change can rewrite the ACL mask, so the ACL /* xattrs/ACLs last: a mode change can rewrite the ACL mask, so the ACL
xattrs must be (re)applied after fchmod. */ xattrs must be (re)applied after fchmod. */
if (apply_xattrs && dir_fd >= 0 && list->xattrs) if (apply_xattrs && dir_fd >= 0 && list->xattrs)
+15
View File
@@ -817,6 +817,21 @@ static FileSaveResult file_save_directory_to_disk(const FileSavePlan* plan, bool
} else if (ok && identity_copy_as_active()) { } else if (ok && identity_copy_as_active()) {
ok = false; ok = false;
} }
/* --fake-super: park the directory's full stat in rsync's reserved
user.rsync.%stat xattr as soon as the directory exists. This makes even a
direct file_save_to_disk_full() caller -- which never runs the deferred
DirTimeList pass -- produce an rsync-readable fake-super record. The record
carries the full mode/uid/gid; the permission bits are replayed by the
deferred pass (never inline, so a restrictive mode cannot block child
creation) and the recorded ownership is never real-chowned. Best-effort:
fake_super_store_fd() logs and skips a failure, never failing the entry. */
if (ok && plan->config && plan->config->fake_super && file->metadata && dir_fd >= 0) {
uint32_t store_uid = 0;
uint32_t store_gid = 0;
identity_resolve_storage_ids((int32_t)file->metadata->uid, (int32_t)file->metadata->gid,
&store_uid, &store_gid);
fake_super_store_fd(dir_fd, store_uid, store_gid, (uint32_t)file->metadata->mode, 0, 0);
}
/* The final source MODE is deliberately NOT applied inline. A restrictive /* The final source MODE is deliberately NOT applied inline. A restrictive
source mode (for example 0555) would make the directory unwritable before source mode (for example 0555) would make the directory unwritable before
its children are created, so a non-root receiver fails each child with its children are created, so a non-root receiver fails each child with
+1 -1
View File
@@ -276,7 +276,7 @@ static bool identity_wire_map_valid(const IdentityMap* map) {
} }
if (map->to < IDENTITY_CURRENT) if (map->to < IDENTITY_CURRENT)
return false; return false;
if (map->to_name && strlen(map->to_name) > 255) if (map->to_name && strlen(map->to_name) > IDENTITY_MAX_NAME_LEN)
return false; return false;
return true; return true;
} }
+5
View File
@@ -6,6 +6,11 @@
#include <stdint.h> #include <stdint.h>
#include <sys/types.h> #include <sys/types.h>
/* Maximum length of a receiver-resolved identity name in a FROM:TO map's TO
* field. Bounded so a malicious/huge name can never cross the wire (see
* identity_wire_map_valid). */
#define IDENTITY_MAX_NAME_LEN 255
/* /*
* Identity mapping: --numeric-ids / --usermap / --groupmap / --chown / --copy-as. * Identity mapping: --numeric-ids / --usermap / --groupmap / --chown / --copy-as.
* *
+221 -260
View File
@@ -47,285 +47,236 @@ bool receive_file_xattrs(File* file, int fd, const Config* config) {
static bool receive_file_payload_into(File* file, int fd, const Config* config, static bool receive_file_payload_into(File* file, int fd, const Config* config,
const char* dest_path, unsigned long long expected_size); const char* dest_path, unsigned long long expected_size);
/* Receive a STATUS_DELTA_DATA response: the sender's delta against the basis we
signed. Deserializes, decompresses and applies the delta (in memory or
through a spool temp file), then receives the metadata/xattr block and
installs the reconstructed payload. Takes ownership of `old_data` and `sig`,
releasing both on every path. */
static File* receive_delta_data_branch(int fd, const Config* config, const char* check_path,
void* old_data, unsigned long long old_size, int basis_fd,
DeltaSignature* sig, bool* failed) {
Data* raw_delta = NULL;
Delta* delta = NULL;
void* new_data = NULL;
char* spool = NULL;
File* file = NULL;
raw_delta = receive_data_limited(fd, MAX_RECEIVE_WHOLE_FILE_SIZE);
if (!raw_delta)
goto fail;
if (config->use_compression &&
!compression_should_skip_with_suffixes(check_path, config->skip_compress_suffixes,
config->skip_compress_set ? config->skip_compress_count
: -1)) {
ProtocolSession* owner = raw_delta->owner;
Data* decompressed = data_decompress_limited(raw_delta, MAX_RECEIVE_WHOLE_FILE_SIZE);
data_destroy(raw_delta);
raw_delta = decompressed;
if (!raw_delta)
goto fail;
/* Charge the decompressed delta to the connection budget (the paired
wire buffer's charge was just released). */
if (!data_charge_session(raw_delta, owner, raw_delta->size))
goto fail;
}
delta = delta_deserialize(raw_delta);
data_destroy(raw_delta);
raw_delta = NULL;
if (!delta)
goto fail;
uint64_t new_size = delta->new_file_size;
if (new_size > SIZE_MAX) {
send_status(fd, STATUS_ERROR);
goto fail;
}
/* Wire-stats tally: bytes taken straight from the basis file (matched
delta blocks) and bytes shipped literally (protocol 2.28.0). Computed
before the delta is destroyed. */
unsigned long long matched = 0;
unsigned long long literal = 0;
for (uint32_t k = 0; k < delta->instruction_count; k++) {
if (delta->instructions[k].type == DELTA_INSTR_BLOCK_MATCH)
matched += delta->instructions[k].match.length;
else if (delta->instructions[k].type == DELTA_INSTR_LITERAL)
literal += delta->instructions[k].literal.length;
}
/* A reconstructed file above the streaming bound is written into a spool
temp file through delta_apply_to_fd; a smaller one keeps the historical
in-memory reconstruction. */
if (new_size > protocol_whole_file_receive_limit()) {
char* dest_path = path_cat(config->receive_root_directory, check_path);
int spool_fd = dest_path ? file_spool_for_payload(dest_path, &spool) : -1;
free(dest_path);
if (spool_fd < 0) {
send_status(fd, STATUS_ERROR);
goto fail;
}
bool applied =
delta_apply_to_fd(old_data, basis_fd, old_size, delta, config->delta_block_size, spool_fd);
if (close(spool_fd) != 0)
applied = false;
delta_destroy(delta);
delta = NULL;
if (!applied) {
send_status(fd, STATUS_ERROR);
goto fail;
}
} else {
new_data = old_data ? delta_apply(old_data, old_size, delta, config->delta_block_size)
: delta_apply_fd(basis_fd, old_size, delta, config->delta_block_size);
delta_destroy(delta);
delta = NULL;
if (!new_data)
goto fail;
}
file = file_create(check_path);
if (!file)
goto fail;
file->matched_bytes = matched;
file->literal_bytes = literal;
if (config->use_metadata) {
int meta_ok = 1;
file->metadata = metadata_receive(fd, &meta_ok);
if (!meta_ok)
goto fail;
}
if (!receive_file_xattrs(file, fd, config))
goto fail;
if (spool) {
Data* reserved = data_create_reserve((size_t)new_size);
if (reserved == NULL) {
send_status(fd, STATUS_ERROR);
goto fail;
}
data_destroy(file->data);
file->data = reserved;
file->basis_copy = spool;
spool = NULL; /* ownership moved into file->basis_copy */
file->data_spool = true;
} else {
Data* replacement = data_create(new_data, (size_t)new_size);
new_data = NULL; /* data_create owns, and frees, the buffer on failure */
if (replacement == NULL) {
send_status(fd, STATUS_ERROR);
goto fail;
}
data_destroy(file->data);
file->data = replacement;
}
free(old_data);
delta_signature_destroy(sig);
return file;
fail:
free(new_data);
if (spool) {
unlink(spool);
free(spool);
}
file_destroy(file);
delta_destroy(delta);
data_destroy(raw_delta);
free(old_data);
delta_signature_destroy(sig);
*failed = true;
return NULL;
}
/* Receive a STATUS_NEXT response: the sender declined the delta and will send
the whole file. Releases the basis signature and snapshot, then receives the
metadata/xattr block and the full payload. Takes ownership of `old_data` and
`sig`, releasing both immediately. */
static File* receive_next_branch(int fd, const Config* config, const char* check_path,
unsigned long long expected_size, void* old_data,
DeltaSignature* sig, bool* failed) {
delta_signature_destroy(sig);
free(old_data);
File* file = file_create(check_path);
if (!file)
goto fail;
if (config->use_metadata) {
int meta_ok = 1;
file->metadata = metadata_receive(fd, &meta_ok);
if (!meta_ok)
goto fail;
}
if (!receive_file_xattrs(file, fd, config))
goto fail;
char* dest_path = path_cat(config->receive_root_directory, check_path);
if (!dest_path)
goto fail;
bool payload_ok = receive_file_payload_into(file, fd, config, dest_path, expected_size);
free(dest_path);
if (!payload_ok)
goto fail;
return file;
fail:
file_destroy(file);
*failed = true;
return NULL;
}
/* Delta handshake dispatcher: sign the basis, ship the signature, then hand the
response off to the matching branch helper. Takes ownership of `old_data`
(and, once created, `sig`); sets `*failed` on every error path. */
static File* receive_delta_file(int fd, const Config* config, const char* check_path, static File* receive_delta_file(int fd, const Config* config, const char* check_path,
void* old_data, unsigned long long old_size, void* old_data, unsigned long long old_size,
unsigned long long expected_size, int basis_fd, bool* failed) { unsigned long long expected_size, int basis_fd, bool* failed) {
if (!old_data && basis_fd < 0) {
free(old_data); /* defensive: a basis source is always provided today */
*failed = true;
return NULL;
}
/* The basis is either an in-memory snapshot (the destination file, bounded) or /* The basis is either an in-memory snapshot (the destination file, bounded) or
* a confined descriptor (a --fuzzy sibling, possibly larger than memory) that * a confined descriptor (a --fuzzy sibling, possibly larger than memory) that
* is signed/applied in bounded chunks. */ * is signed/applied in bounded chunks. */
Data* sig_data = NULL;
Status resp = STATUS_ERROR;
bool sig_sent = false;
/* A delta check needs at least one basis source: the in-memory destination
* snapshot or a confined basis descriptor. */
if (!old_data && basis_fd < 0) {
*failed = true;
return NULL;
}
DeltaSignature* sig = DeltaSignature* sig =
old_data ? delta_signature_create_seeded(old_data, old_size, config->delta_block_size, old_data ? delta_signature_create_seeded(old_data, old_size, config->delta_block_size,
(uint32_t)config->checksum_seed) (uint32_t)config->checksum_seed)
: delta_signature_create_fd_seeded(basis_fd, old_size, config->delta_block_size, : delta_signature_create_fd_seeded(basis_fd, old_size, config->delta_block_size,
(uint32_t)config->checksum_seed); (uint32_t)config->checksum_seed);
if (!sig) { if (!sig)
free(old_data); goto fail;
*failed = true;
return NULL;
}
Data* sig_data = delta_signature_serialize(sig); sig_data = delta_signature_serialize(sig);
if (!sig_data) { if (!sig_data)
delta_signature_destroy(sig); goto fail;
free(old_data);
*failed = true;
return NULL;
}
bool sig_sent = send_status(fd, STATUS_DELTA_SIGNATURE) && send_data(fd, sig_data); sig_sent = send_status(fd, STATUS_DELTA_SIGNATURE) && send_data(fd, sig_data);
data_destroy(sig_data); data_destroy(sig_data);
sig_data = NULL;
if (!sig_sent) { if (!sig_sent || !receive_status(fd, &resp))
delta_signature_destroy(sig); goto fail;
free(old_data);
*failed = true;
return NULL;
}
Status resp; if (resp == STATUS_DELTA_DATA)
if (!receive_status(fd, &resp)) { return receive_delta_data_branch(fd, config, check_path, old_data, old_size, basis_fd, sig,
delta_signature_destroy(sig); failed);
free(old_data);
*failed = true;
return NULL;
}
if (resp == STATUS_DELTA_DATA) { if (resp == STATUS_NEXT)
Data* delta_data = receive_data_limited(fd, MAX_RECEIVE_WHOLE_FILE_SIZE); return receive_next_branch(fd, config, check_path, expected_size, old_data, sig, failed);
if (!delta_data) {
delta_signature_destroy(sig);
free(old_data);
*failed = true;
return NULL;
}
Data* raw_delta = delta_data;
if (config->use_compression &&
!compression_should_skip_with_suffixes(
check_path, config->skip_compress_suffixes,
config->skip_compress_set ? config->skip_compress_count : -1)) {
ProtocolSession* owner = delta_data->owner;
raw_delta = data_decompress_limited(delta_data, MAX_RECEIVE_WHOLE_FILE_SIZE);
data_destroy(delta_data);
if (!raw_delta) {
free(old_data);
delta_signature_destroy(sig);
*failed = true;
return NULL;
}
/* Charge the decompressed delta to the connection budget (the paired
wire buffer's charge was just released). */
if (!data_charge_session(raw_delta, owner, raw_delta->size)) {
data_destroy(raw_delta);
free(old_data);
delta_signature_destroy(sig);
*failed = true;
return NULL;
}
}
Delta* delta = delta_deserialize(raw_delta);
data_destroy(raw_delta);
if (!delta) {
free(old_data);
delta_signature_destroy(sig);
*failed = true;
return NULL;
}
uint64_t new_size = delta->new_file_size;
if (new_size > SIZE_MAX) {
delta_destroy(delta);
free(old_data);
delta_signature_destroy(sig);
send_status(fd, STATUS_ERROR);
*failed = true;
return NULL;
}
/* Wire-stats tally: bytes taken straight from the basis file (matched
delta blocks) and bytes shipped literally (protocol 2.28.0). Computed
before the delta is destroyed. */
unsigned long long matched = 0;
unsigned long long literal = 0;
for (uint32_t k = 0; k < delta->instruction_count; k++) {
if (delta->instructions[k].type == DELTA_INSTR_BLOCK_MATCH)
matched += delta->instructions[k].match.length;
else if (delta->instructions[k].type == DELTA_INSTR_LITERAL)
literal += delta->instructions[k].literal.length;
}
/* A reconstructed file above the streaming bound is written into a spool
temp file through delta_apply_to_fd; a smaller one keeps the historical
in-memory reconstruction. */
void* new_data = NULL;
char* spool = NULL;
if (new_size > protocol_whole_file_receive_limit()) {
char* dest_path = path_cat(config->receive_root_directory, check_path);
int spool_fd = dest_path ? file_spool_for_payload(dest_path, &spool) : -1;
free(dest_path);
if (spool_fd < 0) {
delta_destroy(delta);
free(old_data);
delta_signature_destroy(sig);
send_status(fd, STATUS_ERROR);
*failed = true;
return NULL;
}
bool applied = delta_apply_to_fd(old_data, basis_fd, old_size, delta,
config->delta_block_size, spool_fd);
if (close(spool_fd) != 0)
applied = false;
delta_destroy(delta);
if (!applied) {
unlink(spool);
free(spool);
free(old_data);
delta_signature_destroy(sig);
send_status(fd, STATUS_ERROR);
*failed = true;
return NULL;
}
} else {
new_data = old_data ? delta_apply(old_data, old_size, delta, config->delta_block_size)
: delta_apply_fd(basis_fd, old_size, delta, config->delta_block_size);
delta_destroy(delta);
if (!new_data) {
free(old_data);
delta_signature_destroy(sig);
*failed = true;
return NULL;
}
}
File* file = file_create(check_path);
if (!file) {
free(new_data);
if (spool) {
unlink(spool);
free(spool);
}
free(old_data);
delta_signature_destroy(sig);
*failed = true;
return NULL;
}
file->matched_bytes = matched;
file->literal_bytes = literal;
if (config->use_metadata) {
int meta_ok = 1;
file->metadata = metadata_receive(fd, &meta_ok);
if (!meta_ok) {
file_destroy(file);
free(new_data);
if (spool) {
unlink(spool);
free(spool);
}
free(old_data);
delta_signature_destroy(sig);
*failed = true;
return NULL;
}
}
if (!receive_file_xattrs(file, fd, config)) {
file_destroy(file);
free(new_data);
if (spool) {
unlink(spool);
free(spool);
}
free(old_data);
delta_signature_destroy(sig);
*failed = true;
return NULL;
}
if (spool) {
Data* reserved = data_create_reserve((size_t)new_size);
if (reserved == NULL) {
unlink(spool);
free(spool);
file_destroy(file);
free(old_data);
delta_signature_destroy(sig);
send_status(fd, STATUS_ERROR);
*failed = true;
return NULL;
}
data_destroy(file->data);
file->data = reserved;
file->basis_copy = spool;
file->data_spool = true;
} else {
Data* replacement = data_create(new_data, (size_t)new_size);
if (replacement == NULL) {
file_destroy(file);
free(old_data);
delta_signature_destroy(sig);
send_status(fd, STATUS_ERROR);
*failed = true;
return NULL;
}
data_destroy(file->data);
file->data = replacement;
}
free(old_data);
delta_signature_destroy(sig);
return file;
}
if (resp == STATUS_NEXT) {
delta_signature_destroy(sig);
free(old_data);
File* file = file_create(check_path);
if (!file) {
*failed = true;
return NULL;
}
if (config->use_metadata) {
int meta_ok = 1;
file->metadata = metadata_receive(fd, &meta_ok);
if (!meta_ok) {
file_destroy(file);
*failed = true;
return NULL;
}
}
if (!receive_file_xattrs(file, fd, config)) {
file_destroy(file);
*failed = true;
return NULL;
}
char* dest_path = path_cat(config->receive_root_directory, check_path);
if (!dest_path) {
file_destroy(file);
*failed = true;
return NULL;
}
bool payload_ok = receive_file_payload_into(file, fd, config, dest_path, expected_size);
free(dest_path);
if (!payload_ok) {
file_destroy(file);
*failed = true;
return NULL;
}
return file;
}
send_status(fd, STATUS_ERROR);
fail:
data_destroy(sig_data);
delta_signature_destroy(sig); delta_signature_destroy(sig);
free(old_data); free(old_data);
send_status(fd, STATUS_ERROR);
*failed = true; *failed = true;
return NULL; return NULL;
} }
@@ -1485,20 +1436,24 @@ static IncrementalCheckOutcome incremental_check_try_append_resume(IncrementalCh
if (config->use_metadata) { if (config->use_metadata) {
int meta_ok = 1; int meta_ok = 1;
meta = metadata_receive(fd, &meta_ok); meta = metadata_receive(fd, &meta_ok);
if (!meta_ok) if (!meta_ok) {
file_metadata_destroy(meta);
return INCREMENTAL_ERROR; return INCREMENTAL_ERROR;
}
} }
if (config->use_xattrs) { if (config->use_xattrs) {
int xok = 0; int xok = 0;
append_xattrs = xattr_receive(fd, &xok, config->preserve_acls); append_xattrs = xattr_receive(fd, &xok, config->preserve_acls);
if (!xok) { if (!xok) {
xattr_list_free(append_xattrs); xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR; return INCREMENTAL_ERROR;
} }
} }
Data* tail = receive_data_limited(fd, MAX_RECEIVE_WHOLE_FILE_SIZE); Data* tail = receive_data_limited(fd, MAX_RECEIVE_WHOLE_FILE_SIZE);
if (tail == NULL) { if (tail == NULL) {
xattr_list_free(append_xattrs); xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR; return INCREMENTAL_ERROR;
} }
if (config->use_compression && if (config->use_compression &&
@@ -1510,16 +1465,19 @@ static IncrementalCheckOutcome incremental_check_try_append_resume(IncrementalCh
data_destroy(tail); data_destroy(tail);
if (uncompressed == NULL) { if (uncompressed == NULL) {
xattr_list_free(append_xattrs); xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR; return INCREMENTAL_ERROR;
} }
if (!data_charge_session(uncompressed, owner, uncompressed->size)) { if (!data_charge_session(uncompressed, owner, uncompressed->size)) {
data_destroy(uncompressed); data_destroy(uncompressed);
xattr_list_free(append_xattrs); xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR; return INCREMENTAL_ERROR;
} }
if (uncompressed->size > MAX_FILE_DATA_SIZE) { if (uncompressed->size > MAX_FILE_DATA_SIZE) {
data_destroy(uncompressed); data_destroy(uncompressed);
xattr_list_free(append_xattrs); xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR; return INCREMENTAL_ERROR;
} }
tail = uncompressed; tail = uncompressed;
@@ -1532,6 +1490,7 @@ static IncrementalCheckOutcome incremental_check_try_append_resume(IncrementalCh
send_status(fd, STATUS_ERROR); send_status(fd, STATUS_ERROR);
data_destroy(tail); data_destroy(tail);
xattr_list_free(append_xattrs); xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR; return INCREMENTAL_ERROR;
} }
size_t full_size = (size_t)check_size; size_t full_size = (size_t)check_size;
@@ -1539,6 +1498,7 @@ static IncrementalCheckOutcome incremental_check_try_append_resume(IncrementalCh
if (!full) { if (!full) {
data_destroy(tail); data_destroy(tail);
xattr_list_free(append_xattrs); xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR; return INCREMENTAL_ERROR;
} }
if (old_size > 0 && state->old_data) if (old_size > 0 && state->old_data)
@@ -1553,6 +1513,7 @@ static IncrementalCheckOutcome incremental_check_try_append_resume(IncrementalCh
if (!file) { if (!file) {
free(full); free(full);
xattr_list_free(append_xattrs); xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR; return INCREMENTAL_ERROR;
} }
file->metadata = meta; file->metadata = meta;
+1 -1
View File
@@ -134,7 +134,7 @@ Server* server_create_ex(int port, const ServerBindOptions* bind_opts) {
server->file_descriptor = file_descriptor; server->file_descriptor = file_descriptor;
server->ssl_ctx = NULL; server->ssl_ctx = NULL;
server->max_connections = 100; server->max_connections = SERVER_DEFAULT_MAX_CONNECTIONS;
server->active_connections = 0; server->active_connections = 0;
server->limit_registry = NULL; server->limit_registry = NULL;
+4
View File
@@ -11,6 +11,10 @@
* stored here so the transport layer does not depend on daemon config. */ * stored here so the transport layer does not depend on daemon config. */
struct DaemonLimitRegistry; struct DaemonLimitRegistry;
/* Connection cap applied by server_create_ex() until the daemon's configured
* `max connections` overrides it via server_set_max_connections(). */
#define SERVER_DEFAULT_MAX_CONNECTIONS 100
typedef struct Server { typedef struct Server {
struct sockaddr_storage address; struct sockaddr_storage address;
unsigned int address_length; unsigned int address_length;
+2 -2
View File
@@ -450,7 +450,7 @@ void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, uint
if (len <= 0 || (size_t)len >= sizeof(record)) if (len <= 0 || (size_t)len >= sizeof(record))
return; return;
if (fsetxattr(fd, FAKESUPER_XATTR, record, (size_t)len, 0) != 0) { if (fsetxattr(fd, FAKESUPER_XATTR, record, (size_t)len, 0) != 0) {
log_message(LOG_LEVEL_WARNING, "--fake-super: could not store %s on destination file: %s", log_message(LOG_LEVEL_WARNING, "--fake-super: could not store %s on destination entry: %s",
FAKESUPER_XATTR, strerror(errno)); FAKESUPER_XATTR, strerror(errno));
} }
} }
@@ -550,7 +550,7 @@ bool fake_super_restore_fd(int fd, FileAttrPolicy policy) {
} else if (metadata_mode_for_policy((mode_t)(ul_mode & 0777U), cur.st_mode, policy, &want)) { } else if (metadata_mode_for_policy((mode_t)(ul_mode & 0777U), cur.st_mode, policy, &want)) {
if (fchmod(fd, want) != 0) if (fchmod(fd, want) != 0)
log_message(LOG_LEVEL_WARNING, log_message(LOG_LEVEL_WARNING,
"--fake-super: could not restore mode on destination file: %s", "--fake-super: could not restore mode on destination entry: %s",
strerror(errno)); strerror(errno));
} }
} }
+10 -6
View File
@@ -140,21 +140,25 @@ bool xattr_apply_path_nofollow(int parent_fd, const char* leaf, const FileXattrL
/* --fake-super: write the source uid/gid/mode/rdev record into the reserved /* --fake-super: write the source uid/gid/mode/rdev record into the reserved
* FAKESUPER_XATTR on `fd`, using rsync 3.4.1's exact grammar (see the key * FAKESUPER_XATTR on `fd`, using rsync 3.4.1's exact grammar (see the key
* comment above). `mode` is the full st_mode including its S_IFMT bits. * comment above). `mode` is the full st_mode including its S_IFMT bits.
* Best-effort (logged, never fatal). Only meaningful when metadata was * `fd` may be a regular file, a faked char/block device (written as a regular
* transmitted so the values exist. */ * file), or a DIRECTORY: rsync stores a directory's faked mode/uid/gid in the
* reserved xattr on the directory itself. Best-effort (logged, never fatal).
* Only meaningful when metadata was transmitted so the values exist. */
void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, uint32_t rdev_major, void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, uint32_t rdev_major,
uint32_t rdev_minor); uint32_t rdev_minor);
/* --fake-super replay: parse the FAKESUPER_XATTR record previously written on /* --fake-super replay: parse the FAKESUPER_XATTR record previously written on
* `fd` by fake_super_store_fd and re-apply the recorded permission bits * `fd` by fake_super_store_fd and re-apply the recorded permission bits
* fd-relative. The recorded uid/gid are deliberately NOT chowned for real: * fd-relative. `fd` may be a regular file, a faked device, or a DIRECTORY;
* --fake-super only RECORDS ownership (the caller stores the resolved mapping * fgetxattr/fchmod work identically on a directory descriptor. The recorded
* via identity_resolve_storage_ids), it never performs a real chown. The * uid/gid are deliberately NOT chowned for real: --fake-super only RECORDS
* ownership (the caller stores the resolved mapping via
* identity_resolve_storage_ids), it never performs a real chown. The
* recorded rdev is retained for a later privileged restore but is not acted on * recorded rdev is retained for a later privileged restore but is not acted on
* here. Best-effort: absence of the xattr or a malformed record is a silent * here. Best-effort: absence of the xattr or a malformed record is a silent
* no-op that never fails the transfer. The MODE leg is applied only when * no-op that never fails the transfer. The MODE leg is applied only when
* policy.perms||policy.executability, and the recorded special bits * policy.perms||policy.executability, and the recorded special bits
* (setuid/setgid/sticky) are NOT applied to the real file -- exactly like * (setuid/setgid/sticky) are NOT applied to the real entry -- exactly like
* rsync's fake-super receiver, which stores the full mode in the xattr but * rsync's fake-super receiver, which stores the full mode in the xattr but
* strips the special bits on disk. mtime is not part of the record; the normal * strips the special bits on disk. mtime is not part of the record; the normal
* metadata path carries it (policy.times) exactly as rsync sets the file's own * metadata path carries it (policy.times) exactly as rsync sets the file's own
+66
View File
@@ -7042,6 +7042,72 @@ class TestExtendedAttributes:
f"is not interoperable: ours={rec!r} rsync={out_rec!r}" f"is not interoperable: ours={rec!r} rsync={out_rec!r}"
) )
@pytest.mark.ci
def test_fake_super_directory_rsync_interop(self, shared_server):
"""#319: --fake-super fakes DIRECTORIES too. A recursive -a
--fake-super run must write rsync 3.4.1's `user.rsync.%stat` record on
the directory itself (full mode with S_IFDIR + special bits, rdev 0,0,
uid:gid), replay only the permission bits on disk, and real rsync must
read the tree and re-emit the identical record."""
rsync = shutil.which("rsync")
if rsync is None:
pytest.skip("rsync not installed")
source, dest = self._source_and_dest("fakesuper_dir_interop")
sub = os.path.join(source, "subdir")
os.makedirs(sub)
with open(os.path.join(sub, "f.txt"), "wb") as fh:
fh.write(b"dir interop\n")
if not _xattr_supported(sub):
pytest.skip("filesystem does not support user xattrs")
# A special bit (setgid) is exactly what a fake-super record exists to
# carry: rsync only re-emits a directory record when there is something
# it cannot represent on disk (a special bit, or a mode it would widen
# to keep the owner's rwx). Skip cleanly when the filesystem drops it.
os.chmod(sub, 0o2751)
if stat.S_IMODE(os.stat(sub).st_mode) & 0o7000 == 0:
pytest.skip("filesystem drops directory special bits")
uid = os.stat(sub).st_uid
result, _ = run_client(source, dest, flags=["-a", "--fake-super"],
port=shared_server.port)
assert result.returncode == 0, \
f"-a --fake-super dir sync failed: {(result.stderr or result.stdout)[:300]}"
received = get_dest_received_dir(dest, source)
dst_sub = os.path.join(received, "subdir")
assert os.path.isdir(dst_sub), "the directory entry was not transferred"
rec = os.getxattr(dst_sub, "user.rsync.%stat").decode()
fields = rec.split()
assert len(fields) == 3, f"unexpected rsync fake-super record {rec!r}"
mode_field, rdev_field, owner_field = fields
assert rdev_field == "0,0", f"directory rdev must be 0,0, got {rdev_field!r}"
assert int(mode_field, 8) & 0o170000 == stat.S_IFDIR, (
f"recorded mode {mode_field!r} must carry S_IFDIR"
)
assert int(mode_field, 8) & 0o7777 == 0o2751, (
f"recorded mode {mode_field!r} must carry the full source mode 02751"
)
assert owner_field.split(":")[0] == str(uid), \
f"recorded uid {owner_field!r} != source uid {uid}"
# Permission bits only on disk: the setgid bit stays in the record.
assert stat.S_IMODE(os.stat(dst_sub).st_mode) == 0o751, (
"the directory's special bits must not be installed on disk"
)
# Real rsync reads FastSync's directory record and re-emits it verbatim.
out = os.path.join(TEST_DATA_DIR, "fakesuper_dir_interop_rsync")
clean_dir(out)
rs = subprocess.run([rsync, "-aX", "--fake-super", received + "/", out + "/"],
capture_output=True, text=True, timeout=120)
assert rs.returncode == 0, (
f"rsync could not read FastSync's fake-super directory tree: {rs.stderr[:300]}"
)
out_rec = os.getxattr(os.path.join(out, "subdir"), "user.rsync.%stat").decode()
assert out_rec == rec, (
"rsync re-emitted a different directory fake-super record; FastSync's "
f"grammar is not interoperable: ours={rec!r} rsync={out_rec!r}"
)
@pytest.mark.ci @pytest.mark.ci
def test_directory_xattrs_preserved(self, shared_server): def test_directory_xattrs_preserved(self, shared_server):
"""#286.3: -aX must preserve user.* xattrs on DIRECTORIES, not just files.""" """#286.3: -aX must preserve user.* xattrs on DIRECTORIES, not just files."""
+110
View File
@@ -892,6 +892,114 @@ static void test_symlink_frame_carries_xattrs() {
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
} }
/* --fake-super for DIRECTORIES: rsync stores a directory's faked mode/uid/gid
* in `user.rsync.%stat` on the directory itself. fake_super_store_fd() and
* fake_super_restore_fd() operate on a directory descriptor exactly like a
* file: the full mode (with S_IFDIR + special bits) is recorded, only the
* permission bits are replayed on disk, and the owner is never real-chowned.
* Guarded on filesystem xattr support. */
static void test_fake_super_directory_fd_roundtrip() {
const char* root = "test_fake_super_dirfd_tmp";
const char* path = "test_fake_super_dirfd_tmp/subdir";
rmdir(path);
rmdir(root);
EXPECT_EQ_INT(mkdir(root, 0700), 0);
if (setxattr(root, "user.fastsync-dirprobe", "p", 1, 0) != 0) {
rmdir(root);
return; /* skip silently when the filesystem has no xattr support */
}
removexattr(root, "user.fastsync-dirprobe");
EXPECT_EQ_INT(mkdir(path, 0755), 0);
int fd = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
EXPECT_TRUE(fd >= 0);
FileAttrPolicy policy = {true, true, false, false, true};
/* No record yet: restore is a silent no-op on a directory too. */
EXPECT_FALSE(fake_super_restore_fd(fd, policy));
struct stat before;
EXPECT_EQ_INT(fstat(fd, &before), 0);
fake_super_store_fd(fd, 2222, 3333, S_IFDIR | 01777, 0, 0);
char value[64];
ssize_t got = fgetxattr(fd, FAKESUPER_XATTR, value, sizeof(value));
/* S_IFDIR | 01777 == 0041777 -> "41777 0,0 2222:3333" */
EXPECT_EQ_INT((int)got, 19);
EXPECT_TRUE(got == 19 && memcmp(value, "41777 0,0 2222:3333", 19) == 0);
EXPECT_TRUE(fake_super_restore_fd(fd, policy));
struct stat after;
EXPECT_EQ_INT(fstat(fd, &after), 0);
/* The sticky bit is stored in the record but never installed on disk. */
EXPECT_EQ_INT((int)(after.st_mode & 07777), 0777);
EXPECT_EQ_INT((int)(after.st_mode & (S_ISUID | S_ISGID | S_ISVTX)), 0);
EXPECT_EQ_INT((int)after.st_uid, (int)before.st_uid);
EXPECT_EQ_INT((int)after.st_gid, (int)before.st_gid);
close(fd);
removexattr(path, FAKESUPER_XATTR);
rmdir(path);
rmdir(root);
}
/* The deferred directory-metadata pass is where a recursive -a --fake-super
* transfer stamps each directory: dir_metadata_list_apply() must park the
* directory's full stat in the reserved xattr and replay only its permission
* bits on disk. This is the recursive-path counterpart of the explicit
* --dirs store in file_save_directory_to_disk(). Guarded on xattr support. */
static void test_fake_super_directory_deferred_apply() {
const char* root = "test_fake_super_dirdir_tmp";
const char* leaf = "subdir";
const char* path = "test_fake_super_dirdir_tmp/subdir";
rmdir(path);
rmdir(root);
EXPECT_EQ_INT(mkdir(root, 0700), 0);
if (setxattr(root, "user.fastsync-dirprobe", "p", 1, 0) != 0) {
rmdir(root);
return; /* skip silently when the filesystem has no xattr support */
}
removexattr(root, "user.fastsync-dirprobe");
EXPECT_EQ_INT(mkdir(path, 0755), 0);
FileMetadata m;
memset(&m, 0, sizeof(m));
m.mode = S_IFDIR | 02751;
m.uid = 1001;
m.gid = 1002;
m.mtime_sec = 1234567890;
Config* config = config_create();
EXPECT_NOT_NULL(config);
config->use_metadata = true;
config->preserve_perms = true;
config->preserve_times = true;
config->fake_super = true;
identity_clear_active();
DirTimeList list;
dir_time_list_init(&list);
EXPECT_TRUE(dir_time_list_add(&list, leaf, &m, NULL));
dir_metadata_list_apply(&list, root, config);
dir_time_list_free(&list);
char value[64];
ssize_t got = getxattr(path, FAKESUPER_XATTR, value, sizeof(value));
/* S_IFDIR | 02751 -> "42751 0,0 1001:1002" (resolved ids == source ids). */
EXPECT_EQ_INT((int)got, 19);
EXPECT_TRUE(got == 19 && memcmp(value, "42751 0,0 1001:1002", 19) == 0);
struct stat st;
EXPECT_EQ_INT(stat(path, &st), 0);
/* Only the permission bits land on disk; setgid stays in the record. */
EXPECT_EQ_INT((int)(st.st_mode & 07777), 0751);
EXPECT_EQ_INT((int)(st.st_mode & (S_ISUID | S_ISGID | S_ISVTX)), 0);
config_delete(config);
removexattr(path, FAKESUPER_XATTR);
rmdir(path);
rmdir(root);
}
void test_xattr() { void test_xattr() {
test_xattr_list_clone(); test_xattr_list_clone();
test_xattr_capture_symlink_nofollow(); test_xattr_capture_symlink_nofollow();
@@ -910,5 +1018,7 @@ void test_xattr() {
test_fake_super_rsync_format(); test_fake_super_rsync_format();
test_fake_super_no_real_chown(); test_fake_super_no_real_chown();
test_fake_super_storage_resolution(); test_fake_super_storage_resolution();
test_fake_super_directory_fd_roundtrip();
test_fake_super_directory_deferred_apply();
test_file_save_directory_applies_xattrs(); test_file_save_directory_applies_xattrs();
} }