Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13627e82fc | ||
|
|
09ab81120e | ||
|
|
2a7afbda0a | ||
|
|
bab6fcc706 | ||
|
|
6a426cffa8 | ||
|
|
b74182c7c1 | ||
|
|
f2a8eeaea7 | ||
|
|
18d7d495cf | ||
|
|
59d20e867a | ||
|
|
cc931790e9 | ||
|
|
60776b78fc | ||
|
|
52ad0aa512 | ||
|
|
759ffea117 | ||
|
|
ef37c2b988 |
+12
-8
File diff suppressed because one or more lines are too long
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user