fix(protocol): atomic client-msg flag, sanitize peer messages, flush before terminal, serialization probe

This commit is contained in:
2026-09-23 23:56:31 +02:00
parent b414f197af
commit 729f3ef8e1
11 changed files with 201 additions and 44 deletions
+9 -2
View File
@@ -495,6 +495,7 @@ int send_dry_run_remote(Config* config) {
protocol_session_bind(&session);
int ret = 1;
bool partial = false;
time_t dry_start = time(NULL);
ReceiverStats dry_stats;
memset(&dry_stats, 0, sizeof(dry_stats));
@@ -653,8 +654,14 @@ int send_dry_run_remote(Config* config) {
if (!receive_status(client->file_descriptor, &status))
goto dry_fail;
}
if (status != STATUS_OK)
/* A per-entry receiver failure is rsync's PARTIAL transfer (exit 23), not a
hard failure: a dry run transfers nothing, but keep the verdict consistent
with the normal path instead of treating it as a protocol error. */
if (status == STATUS_PARTIAL) {
partial = true;
} else if (status != STATUS_OK) {
goto dry_fail;
}
if (!config->quiet) {
if (config->human_readable)
printf("Total: %d files, %s\n", file_count,
@@ -672,7 +679,7 @@ int send_dry_run_remote(Config* config) {
dry_transfer.literal_data = total_bytes;
report_transfer_stats(config, &dry_transfer, dry_start, &dry_stats);
}
ret = io_error ? 1 : 0;
ret = io_error ? 1 : (partial ? 23 : 0);
dry_fail:
if (dry_manifest)
+29 -9
View File
@@ -1213,8 +1213,11 @@ static ArrayList* client_msg_queue = NULL; /* owns char* */
static size_t client_msg_bytes = 0;
/* True only while a live transfer session exists: before the connection is up
(or after it drops) the sink declines so log_message falls back to local
output, matching rsync's documented fallback. */
static bool client_msg_active = false;
output, matching rsync's documented fallback. Written by the sender thread
(client_messages_activate) and read by scanner worker threads in
client_msg_enqueue, so it must be atomic: the queue itself stays guarded by
client_msg_mutex, but the flag is polled before taking that lock. */
static _Atomic bool client_msg_active = false;
static void client_msg_mutex_init(void) {
mtx_init(&client_msg_mutex, mtx_plain);
@@ -1229,20 +1232,26 @@ void client_messages_install(void) {
mtx_lock(&client_msg_mutex);
if (!client_msg_queue)
client_msg_queue = array_list_create(free);
bool ready = client_msg_queue != NULL;
mtx_unlock(&client_msg_mutex);
log_set_client_msg_sink(client_msg_enqueue);
/* Only arm the sink once the queue exists; on allocation failure leave the
sink uninstalled so log_message keeps writing locally instead of handing
messages to a sink that would silently drop them. */
if (ready)
log_set_client_msg_sink(client_msg_enqueue);
}
void client_messages_activate(bool active) {
client_msg_active = active;
atomic_store(&client_msg_active, active);
}
/* log_message sink: takes ownership (queues) the message when a session is
* live; returns false otherwise so the caller writes it locally. */
static bool client_msg_enqueue(const char* message) {
bool active = atomic_load(&client_msg_active);
if (!message || message[0] == '\0')
return client_msg_active;
if (!client_msg_active)
return active;
if (!active)
return false;
size_t len = strlen(message);
call_once(&client_msg_mutex_once, client_msg_mutex_init);
@@ -1273,8 +1282,19 @@ void client_flush_client_messages(int fd) {
call_once(&client_msg_mutex_once, client_msg_mutex_init);
mtx_lock(&client_msg_mutex);
ArrayList* pending = client_msg_queue;
client_msg_queue = array_list_create(free);
client_msg_bytes = 0;
if (pending) {
ArrayList* fresh = array_list_create(free);
if (fresh) {
client_msg_queue = fresh;
} else {
/* No memory for a replacement queue: stop queuing new diagnostics (they
fall back to local output) and drain this batch below so nothing is
silently dropped. */
client_msg_queue = NULL;
log_set_client_msg_sink(NULL);
}
client_msg_bytes = 0;
}
mtx_unlock(&client_msg_mutex);
if (!pending)
return;
@@ -1289,7 +1309,7 @@ void client_flush_client_messages(int fd) {
/* Tear down the sink after a transfer and free anything still queued. */
void client_messages_end(void) {
log_set_client_msg_sink(NULL);
client_msg_active = false;
atomic_store(&client_msg_active, false);
call_once(&client_msg_mutex_once, client_msg_mutex_init);
mtx_lock(&client_msg_mutex);
ArrayList* pending = client_msg_queue;
+17
View File
@@ -255,8 +255,18 @@ static bool finalize_transfer(Client* client, const Config* config, ArrayList* r
*delete_limit_out = false;
if (partial_out)
*partial_out = false;
/* --stderr=client: the receiver consumes frames until it reads
STATUS_FINISHED, after which it no longer reads. Flush every diagnostic
queued during the transfer here -- the last frame boundary at which the
peer is still reading -- so nothing is stranded in the queue. */
client_flush_client_messages(client->file_descriptor);
if (!send_status(client->file_descriptor, STATUS_FINISHED))
return false;
/* Past STATUS_FINISHED the receiver has stopped reading, so any diagnostic
logged from here on (notably the STATUS_PARTIAL warning below) can no
longer be forwarded. Deactivate the channel so those messages fall back
to local output instead of being queued for a closed peer and lost. */
client_messages_activate(false);
/* The receiver emits its optional wire-stats frame (protocol 2.25.0) FIRST,
then any per-file --remove-source-files acks, then the terminal status. */
Status status;
@@ -899,6 +909,13 @@ static int send_chunk_with_removal(Client* client, Chunk* chunk, Config* config,
if (chunk->items[i] == NULL)
continue;
transfer_stats_note_entry(stats, chunk->items[i]);
/* Output parity: probe each entry's ancestor directories' destination
state before emitting its itemize line, exactly as the non-serialized
loop does. Without this, dest_state.known stays false and -i/-P
renders an existing dir/symlink as created instead of `.d..t...` (or
suppressing it). */
if (!client_change_probe_ancestors(config, chunk->items[i], client->file_descriptor))
return -1;
/* The chunk-serialization path emits no --progress name lines, so only
feed -i/--out-format its ancestor directory lines here. */
if (config->itemize_changes || config->out_format != NULL)