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 skipped = 0;
ArrayList* missing_dest = NULL;
if (config->delete_missing_args) {
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);
if (!client_prepare_files_from(config, &missing_dest, &skipped))
return -1;
}
PreparedScanner prepared;
if (!prepare_scanner(config, 0, &prepared)) {
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 from_skipped = 0;
ArrayList* missing_args = NULL;
if (config->delete_missing_args) {
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);
if (!client_prepare_files_from(config, &missing_args, &from_skipped))
return 1;
}
if (missing_args)
array_list_delete(missing_args);
/* A live session may follow, so arm graceful abort handling. */
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 (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);
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;
bool partial = false;
+52 -36
View File
@@ -136,6 +136,50 @@ void disconnect_transfer_client(Client* 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.) */
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) {
PipelineContextSender* context = (PipelineContextSender*)pipeline_context;
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 (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);
mark_sender_done(context);
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);
if (!config_send(client->file_descriptor, context->config)) {
pipeline_cancel(context);
@@ -2035,35 +2072,20 @@ static int send_files_impl(Config* config) {
shielded -- rsync's `-d DIR/ --delete`. */
state.delete_per_dir = config->use_delete && config_delete_timing_per_dir(config);
int skipped = 0;
if (config->delete_missing_args) {
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);
if (!client_prepare_files_from(config, &state.missing_args, &skipped))
return 1;
}
/* 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. */
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 (config->transport == TRANSPORT_TCP)
log_message(LOG_LEVEL_ERROR, "could not connect to server%s",
config->use_tls ? " via TLS" : "");
if (state.missing_args)
array_list_delete(state.missing_args);
return 1;
}
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);
int ret = 1;
@@ -2099,16 +2121,8 @@ static int send_files_multithreaded_impl(Config* config) {
: send_dry_run_manifest(config);
ArrayList* missing_args = NULL;
int skipped = 0;
if (config->delete_missing_args) {
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);
if (!client_prepare_files_from(config, &missing_args, &skipped))
return 1;
}
/* Armed only once a session may go live (see send_files). */
client_set_abort_armed(true);
@@ -2137,6 +2151,8 @@ static int send_files_multithreaded_impl(Config* config) {
queue_destroy(q1);
if (q2)
queue_destroy(q2);
if (missing_args)
array_list_delete(missing_args);
return 1;
}
PipelineContextSender* context = pipeline_context_sender_create(config, q1, q2);
+15
View File
@@ -13,6 +13,7 @@
#include "delta.h"
#include "format.h"
#include "log.h"
#include "protocol.h"
#include "scanner.h"
#include <stdatomic.h>
#include <stdbool.h>
@@ -91,6 +92,20 @@ void client_messages_end(void);
/* client_send.c */
void receive_daemon_motd(Client* 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);
int incremental_check(Client* client, File* file, const Config* config, DeltaSignature** out_sig,
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;
if (scanner->options.chunk_size == 0)
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) {
free(scanner);
return NULL;
@@ -1026,7 +1026,7 @@ static ScannerAction scanner_process_entry(DirectoryScanner* scanner, ArrayList*
Chunk** out_chunk) {
const char* name = sorted->name;
ScannerEntry* inspected = &sorted->entry;
char* cur_path = inspected->path;
const char* cur_path = inspected->path;
struct stat stats = inspected->stats;
/* --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);
return SCANNER_ACTION_CONTINUE;
}
File* file = file_create(cur_path);
if (file == NULL) {
free(rel_copy);
free(inspected->link_target);
inspected->link_target = NULL;
/* Entry construction (data size, -R wire path, special/devices, hardlink
group, metadata, xattrs) is shared with the parallel scanner. */
File* file = NULL;
bool build_failed = false;
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;
if (status == SCANNER_BUILD_SKIP)
return SCANNER_ACTION_CONTINUE;
}
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);
if (status != SCANNER_BUILD_OK) {
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)) {
free(rel_copy);
file_destroy(file);
scanner->failed = true;
return SCANNER_ACTION_BREAK;
@@ -1163,14 +1125,12 @@ static ScannerAction scanner_process_entry(DirectoryScanner* scanner, ArrayList*
scanner->current_dir_produced = true;
*chunk_data_size += file->data->size;
if (*chunk_data_size > scanner->options.chunk_size) {
free(rel_copy);
Chunk* result = chunk_data_to_chunk(chunk_data);
if (!result)
scanner->failed = true;
*out_chunk = result;
return SCANNER_ACTION_CHUNK;
}
free(rel_copy);
return SCANNER_ACTION_CONTINUE;
}
+5
View File
@@ -19,6 +19,11 @@
* keeps one transfer from spawning an unbounded pool on a very large machine. */
#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 {
bool use_metadata;
/* 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
* 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. */
void scanner_capture_xattrs(const DirectoryScanner* scanner, File* file) {
if (!scanner || !file || !(scanner->options.preserve_xattrs || scanner->options.preserve_acls))
void scanner_capture_xattrs_opts(const ScannerOptions* options, File* file) {
if (!options || !file || !(options->preserve_xattrs || options->preserve_acls))
return;
file->xattrs = file->is_symlink
? xattr_capture_path_nofollow(file->path, scanner->options.preserve_acls)
: xattr_capture_path(file->path, scanner->options.preserve_acls);
file->xattrs = file->is_symlink ? xattr_capture_path_nofollow(file->path, options->preserve_acls)
: xattr_capture_path(file->path, 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
* 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
* first member is left untouched (data present, link_first). Allocation
* failure is fatal: the scanner is marked failed. */
void scanner_assign_hardlink(DirectoryScanner* scanner, HardLinkTable* table, File* file,
const struct stat* stats) {
* first member is left untouched (data present, link_first). Returns false on
* allocation failure (the caller marks the scan failed); the File stays usable
* either way. */
bool scanner_assign_hardlink(HardLinkTable* table, File* file, const struct stat* stats) {
if (!table || !file || !stats)
return;
return true;
int gid;
bool is_first;
char* first_path = NULL;
if (!hardlink_table_assign(table, file_wire_path(file), stats->st_dev, stats->st_ino, &gid,
&is_first, &first_path)) {
if (scanner)
scanner->failed = true;
return;
}
&is_first, &first_path))
return false;
file->link_group = gid;
file->link_first = is_first;
if (!is_first) {
@@ -319,6 +321,7 @@ void scanner_assign_hardlink(DirectoryScanner* scanner, HardLinkTable* table, Fi
} else {
free(first_path);
}
return true;
}
/* 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);
}
/* 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
* directory: `[sender] skipping mount-point dir NAME` (the client is the
* 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 */
} 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 */
void filter_node_destroy(void* item);
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,
bool per_dir_filters, bool exclude_filter_files, bool* protect_out);
void scanner_capture_xattrs(const DirectoryScanner* scanner, File* file);
void scanner_assign_hardlink(DirectoryScanner* scanner, HardLinkTable* table, File* file,
const struct stat* stats);
void scanner_capture_xattrs_opts(const ScannerOptions* options, File* file);
bool scanner_assign_hardlink(HardLinkTable* table, File* file, const struct stat* stats);
ScannerSpecial scanner_prepare_special(bool preserve_devices, bool preserve_specials,
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);
void scanner_note_nonreg(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. */
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)
return false;
atomic_init(&ps->cancelled, false);
@@ -210,6 +210,157 @@ static Chunk* batch_files(ArrayList* files, unsigned long long chunk_size, Queue
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. */
static void scan_root_entry(const ScannerOptions* options, const FilterNode* root_node,
const char* root_directory, const struct dirent* entry,
@@ -223,51 +374,16 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
return;
}
if (inspection == 0) {
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) {
/* 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);
}
}
}
scan_root_record_skipped(options, root_directory, entry->d_name, &inspected, ps);
return;
}
char* cur_path = inspected.path;
struct stat st = inspected.stats;
bool is_dir = inspected.is_directory;
char* rel = str_dup(entry->d_name);
if (!rel) {
free(cur_path);
ps->failed = true;
return;
goto done;
}
bool is_dir = inspected.is_directory;
bool protect = false;
bool passes = entry_passes_selection(options->file_list, options->base_filters, root_node, rel,
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. */
bool use_rel = options->relative && options->file_list != NULL;
if (!passes || protect) {
/* --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, 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 (!scan_root_record_protection(options, rel, entry->d_name, cur_path, protect, passes,
use_rel, ps)) {
ps->failed = true;
goto done;
}
if (!passes) {
scanner_note_filter(options, entry->d_name);
free(rel);
free(cur_path);
return;
goto done;
}
}
if (is_dir) {
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);
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;
scan_root_dir(options, cur_path, rel, &inspected.stats, root_files, subdirs, root_dev, ps);
} else if (S_ISCHR(inspected.stats.st_mode) || S_ISBLK(inspected.stats.st_mode) ||
S_ISFIFO(inspected.stats.st_mode) || S_ISSOCK(inspected.stats.st_mode)) {
scan_root_special(options, &inspected, rel, root_files, ps);
} else {
file->data->size = st.st_size;
}
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;
scan_root_file(options, &inspected, rel, root_files, ps);
}
done:
free(rel);
free(cur_path);
free(inspected.link_target);
}
/* Scan the root directory itself, collecting root files and subdirectories.
+284 -244
View File
@@ -1306,6 +1306,284 @@ static bool daemonize(void) {
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[]) {
/* 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
@@ -1325,250 +1603,12 @@ int main(int argc, char* argv[]) {
return 1;
}
/* --hash-credentials: standalone offline tool; read user:password lines and
* emit new-format credential-store lines, then exit. */
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;
}
if (opts.hash_credentials_file)
return run_hash_credentials_tool(&opts);
int exit_code = 0;
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);
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;
configure_server_process(&opts);
if (opts.stdio_mode)
return run_stdio_server(&opts);
return run_daemon_server(&opts);
}
#endif
+258 -186
View File
@@ -224,6 +224,235 @@ typedef struct {
void* observer_context; /* DELETE mode */
} 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)
or record the paths that WOULD be removed (LIST mode), recursing into every
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
ascending order) rather than readdir() order, so `--max-delete` leaves the
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,
const PathIndex* dirs, DeleteWalkState* state,
const DeleteSkipEntry* skips, int skip_count,
const DeleteProtectRules* protect, bool parent_deletable,
bool* all_removed) {
static bool delete_walk_fd(int dirfd, const char* rel_path, const DeleteWalkContext* ctx,
bool parent_deletable, bool* all_removed) {
DeleteDirEntry* entries = NULL;
size_t count = 0;
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
`parent_deletable` flag carries that down the recursion so dest-only
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';
/* 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)
dir_count++;
/* Classify every entry up front (the verdict does not depend on processing
order) so the ordered passes below can act on it. */
for (size_t i = 0; i < count; i++) {
char* child_rel = path_cat((char*)rel_path, entries[i].name);
if (!child_rel) {
operation_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, 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);
}
if (!delete_walk_classify(rel_path, entries, count, ctx, deletable, at_root, shielded, is_extra,
&local_survives))
operation_ok = false;
if (!delete_walk_extra_dirs(dirfd, rel_path, entries, dir_count, ctx, deletable, is_extra,
&local_survives))
operation_ok = false;
if (!delete_walk_extra_files(dirfd, rel_path, entries, dir_count, count, ctx, is_extra,
&local_survives))
operation_ok = false;
if (!delete_walk_kept_dirs(dirfd, rel_path, entries, dir_count, ctx, deletable, is_extra,
shielded, &local_survives))
operation_ok = false;
free(shielded);
free(is_extra);
@@ -504,8 +566,13 @@ bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
.recorded = &recorded,
.observer = NULL,
.observer_context = NULL};
bool ok = delete_walk_fd(rootfd, "", &keep, have_dirs ? &dirs : NULL, &state, skips, skip_count,
protect, false, &all_removed);
DeleteWalkContext ctx = {.keep = &keep,
.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)
ok = false;
path_index_free(&keep);
@@ -557,8 +624,13 @@ DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const Arr
.recorded = NULL,
.observer = observer,
.observer_context = observer_context};
bool ok = delete_walk_fd(rootfd, "", &keep, have_dirs ? &dirs : NULL, &state, skips, skip_count,
protect, false, &all_removed);
DeleteWalkContext ctx = {.keep = &keep,
.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)
ok = false;
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
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. */
/* 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,
const DeleteManifest* manifest,
DeleteBudgetState* budget,
@@ -246,141 +406,8 @@ static bool delete_missing_args_budgeted_observed(const Config* config,
bool ok = true;
for (int i = 0; i < manifest->missing->size; i++) {
const char* rel = (const char*)manifest->missing->items[i];
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;
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)
if (delete_one_missing_arg(config, rel, &skips, budget, observer, observer_context, &ok) ==
MISSING_ARG_STOP)
break;
}
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
* requested: -p/--perms (directory modes), -t/--times (directory mtimes,
* unless -O/--omit-dir-times suppresses them), -o/-g (directory ownership),
* or -X/-A (directory xattrs/ACLs). --atimes/-U alone does not pull
* directory metadata (matching the original dir-time bundle). */
* -X/-A (directory xattrs/ACLs), or --fake-super (whose reserved %stat record
* 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 &&
(config->preserve_perms || (config->preserve_times && !config->omit_dir_times) ||
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) {
@@ -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_mode = config->preserve_perms;
bool apply_xattrs = config->use_xattrs;
bool apply_fake_super = config->fake_super;
/* Ownership is applied through the active identity snapshot (which no-ops
* unless an ownership request is active), and xattrs only when -X/-A was
* negotiated. Times/mode keep their own per-attribute gates. */
bool have_any = apply_times || apply_mode || apply_xattrs || identity_active_enabled();
* unless an ownership request is active), xattrs only when -X/-A was
* negotiated, and the --fake-super record whenever the flag is active.
* 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)
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++) {
char* dir_path = path_cat(root_directory, list->paths[i]);
if (!dir_path)
@@ -260,38 +269,59 @@ void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory
free(escaped_path);
}
}
if (apply_mode) {
mode_t dir_mode = list->entries[i].mode;
bool mode_ready = true;
if (config->chmod_spec && *config->chmod_spec &&
!chmod_apply(dir_mode, config->chmod_spec, &dir_mode)) {
/* The final directory mode (after any --chmod) is computed once so the
--fake-super record can carry it even when the on-disk replay is
restricted to the permission bits below. */
mode_t dir_mode = list->entries[i].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());
log_message(LOG_LEVEL_WARNING, "Failed to apply --chmod to directory %s",
escaped_path ? escaped_path : "<allocation failed>");
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);
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 must be (re)applied after fchmod. */
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()) {
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
source mode (for example 0555) would make the directory unwritable before
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)
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 true;
}
+5
View File
@@ -6,6 +6,11 @@
#include <stdint.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.
*
+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,
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,
void* old_data, unsigned long long old_size,
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
* a confined descriptor (a --fuzzy sibling, possibly larger than memory) that
* 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 =
old_data ? delta_signature_create_seeded(old_data, old_size, config->delta_block_size,
(uint32_t)config->checksum_seed)
: delta_signature_create_fd_seeded(basis_fd, old_size, config->delta_block_size,
(uint32_t)config->checksum_seed);
if (!sig) {
free(old_data);
*failed = true;
return NULL;
}
if (!sig)
goto fail;
Data* sig_data = delta_signature_serialize(sig);
if (!sig_data) {
delta_signature_destroy(sig);
free(old_data);
*failed = true;
return NULL;
}
sig_data = delta_signature_serialize(sig);
if (!sig_data)
goto fail;
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);
sig_data = NULL;
if (!sig_sent) {
delta_signature_destroy(sig);
free(old_data);
*failed = true;
return NULL;
}
if (!sig_sent || !receive_status(fd, &resp))
goto fail;
Status resp;
if (!receive_status(fd, &resp)) {
delta_signature_destroy(sig);
free(old_data);
*failed = true;
return NULL;
}
if (resp == STATUS_DELTA_DATA)
return receive_delta_data_branch(fd, config, check_path, old_data, old_size, basis_fd, sig,
failed);
if (resp == STATUS_DELTA_DATA) {
Data* delta_data = receive_data_limited(fd, MAX_RECEIVE_WHOLE_FILE_SIZE);
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;
}
if (resp == STATUS_NEXT)
return receive_next_branch(fd, config, check_path, expected_size, old_data, sig, failed);
send_status(fd, STATUS_ERROR);
fail:
data_destroy(sig_data);
delta_signature_destroy(sig);
free(old_data);
send_status(fd, STATUS_ERROR);
*failed = true;
return NULL;
}
@@ -1485,20 +1436,24 @@ static IncrementalCheckOutcome incremental_check_try_append_resume(IncrementalCh
if (config->use_metadata) {
int meta_ok = 1;
meta = metadata_receive(fd, &meta_ok);
if (!meta_ok)
if (!meta_ok) {
file_metadata_destroy(meta);
return INCREMENTAL_ERROR;
}
}
if (config->use_xattrs) {
int xok = 0;
append_xattrs = xattr_receive(fd, &xok, config->preserve_acls);
if (!xok) {
xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR;
}
}
Data* tail = receive_data_limited(fd, MAX_RECEIVE_WHOLE_FILE_SIZE);
if (tail == NULL) {
xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR;
}
if (config->use_compression &&
@@ -1510,16 +1465,19 @@ static IncrementalCheckOutcome incremental_check_try_append_resume(IncrementalCh
data_destroy(tail);
if (uncompressed == NULL) {
xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR;
}
if (!data_charge_session(uncompressed, owner, uncompressed->size)) {
data_destroy(uncompressed);
xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR;
}
if (uncompressed->size > MAX_FILE_DATA_SIZE) {
data_destroy(uncompressed);
xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR;
}
tail = uncompressed;
@@ -1532,6 +1490,7 @@ static IncrementalCheckOutcome incremental_check_try_append_resume(IncrementalCh
send_status(fd, STATUS_ERROR);
data_destroy(tail);
xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR;
}
size_t full_size = (size_t)check_size;
@@ -1539,6 +1498,7 @@ static IncrementalCheckOutcome incremental_check_try_append_resume(IncrementalCh
if (!full) {
data_destroy(tail);
xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR;
}
if (old_size > 0 && state->old_data)
@@ -1553,6 +1513,7 @@ static IncrementalCheckOutcome incremental_check_try_append_resume(IncrementalCh
if (!file) {
free(full);
xattr_list_free(append_xattrs);
file_metadata_destroy(meta);
return INCREMENTAL_ERROR;
}
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->ssl_ctx = NULL;
server->max_connections = 100;
server->max_connections = SERVER_DEFAULT_MAX_CONNECTIONS;
server->active_connections = 0;
server->limit_registry = NULL;
+4
View File
@@ -11,6 +11,10 @@
* stored here so the transport layer does not depend on daemon config. */
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 {
struct sockaddr_storage address;
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))
return;
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));
}
}
@@ -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)) {
if (fchmod(fd, want) != 0)
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));
}
}
+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
* 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.
* Best-effort (logged, never fatal). Only meaningful when metadata was
* transmitted so the values exist. */
* `fd` may be a regular file, a faked char/block device (written as a regular
* 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,
uint32_t rdev_minor);
/* --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-relative. The recorded 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
* fd-relative. `fd` may be a regular file, a faked device, or a DIRECTORY;
* fgetxattr/fchmod work identically on a directory descriptor. The recorded
* 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
* 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
* 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
* 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
+66
View File
@@ -7042,6 +7042,72 @@ class TestExtendedAttributes:
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
def test_directory_xattrs_preserved(self, shared_server):
"""#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);
}
/* --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() {
test_xattr_list_clone();
test_xattr_capture_symlink_nofollow();
@@ -910,5 +1018,7 @@ void test_xattr() {
test_fake_super_rsync_format();
test_fake_super_no_real_chown();
test_fake_super_storage_resolution();
test_fake_super_directory_fd_roundtrip();
test_fake_super_directory_deferred_apply();
test_file_save_directory_applies_xattrs();
}