fix(receiver): harden idle-progress definition, single error frame, sendfile timeout

This commit is contained in:
2026-09-13 03:46:20 +02:00
parent b16349b81e
commit ffa1d24625
7 changed files with 69 additions and 26 deletions
+28 -9
View File
@@ -203,22 +203,41 @@ bool receiver_time_limit_exceeded(const struct timespec* session_start,
return false;
}
/* A frame proves forward progress only when it cannot be fabricated for free.
* KEEPALIVE/ABORT are pure liveness, and CHECK_BATCH/DIR_TIMES may carry zero
* entries, so a peer must not be able to hold a connection slot forever by
* merely emitting empty frames. */
static bool status_counts_as_progress(Status status) {
switch (status) {
case STATUS_KEEPALIVE:
case STATUS_ABORT:
case STATUS_CHECK_BATCH:
case STATUS_DIR_TIMES:
return false;
default:
return true;
}
}
/* Refresh the progress timestamp for a forward-moving frame and enforce the
* bounds above. Returns false (after best-effort STATUS_ERROR) when the
* connection must be dropped. */
* bounds above. Returns false when the connection must be dropped; the
* terminal STATUS_ERROR is sent only when the sink owns error reporting (the
* -m sink sets send_error=false so the main thread emits exactly one). */
static bool receiver_note_status(const struct timespec* session_start,
struct timespec* last_progress, Status status,
int file_descriptor) {
struct timespec* last_progress, Status status, int file_descriptor,
const ReceiverSink* sink) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (status != STATUS_KEEPALIVE && status != STATUS_ABORT)
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
now = *last_progress;
if (status_counts_as_progress(status))
*last_progress = now;
if (!receiver_time_limit_exceeded(session_start, last_progress, &now))
return true;
log_message(LOG_LEVEL_ERROR,
"Receive session exceeded its time bound (idle %us / total %us); aborting connection",
g_max_session_idle_sec, g_max_session_wall_sec);
send_status(file_descriptor, STATUS_ERROR);
if (!sink || sink->send_error)
send_status(file_descriptor, STATUS_ERROR);
return false;
}
@@ -248,7 +267,7 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
struct timespec last_progress;
clock_gettime(CLOCK_MONOTONIC, &session_start);
last_progress = session_start;
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor))
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor, sink))
return -1;
bool early_delete = config_delete_timing_early(config);
/* Parked keep-set for the late/commit timing. Every exit path below frees it
@@ -349,7 +368,7 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
next_status:
if (!receive_status(file_descriptor, &status))
goto receive_error;
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor))
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor, sink))
goto fail;
}
if (status != STATUS_FINISHED) {
+5 -4
View File
@@ -609,10 +609,11 @@ void handler(int file_descriptor) {
if (gate_ctx.super_mode_override != -1)
config->super_mode = (SuperMode)gate_ctx.super_mode_override;
protocol_set_8_bit_output(config->eight_bit_output);
/* Honor the negotiated --timeout for every protocol frame from here on (the
* config handshake itself used the built-in 60 s window). A positive value
* also tightens the socket SO_RCVTIMEO/SO_SNDTIMEO already applied by the
* transport; 0 leaves both built-in defaults in place. */
/* Server-side per-message protocol deadline for every frame from here on.
* `timeout` is not serialized, so this is the server's own config (the server
* has no --timeout CLI and defaults it to 0): the built-in 60 s window stays
* in effect. A client's --timeout tightens only that client's own protocol
* I/O and the server's socket read/write timeout is the transport default. */
protocol_session_set_io_timeout(&session, config->timeout);
if (!authorized_root) {
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
+1 -1
View File
@@ -145,7 +145,7 @@ bool file_send_sendfile_with_skip(File* file, int file_descriptor, bool use_meta
off_t offset = 0;
struct timespec deadline;
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_sec += 60;
deadline.tv_sec += protocol_get_io_timeout_sec();
while ((unsigned long long)offset < file_size) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
+6
View File
@@ -87,6 +87,12 @@ void protocol_session_set_io_timeout(ProtocolSession* session, int sec) {
session->io_timeout_sec = sec;
}
int protocol_get_io_timeout_sec(void) {
const ProtocolSession* session = bound_session ? bound_session : &legacy_io_session;
int sec = session->io_timeout_sec;
return sec > 0 ? sec : RECEIVE_TIMEOUT_SEC;
}
void protocol_session_set_max_alloc(ProtocolSession* session, unsigned long long max_alloc) {
if (!session)
session = bound_session ? bound_session : &legacy_io_session;
+4
View File
@@ -152,6 +152,10 @@ void protocol_session_set_max_alloc(ProtocolSession* session, unsigned long long
* An explicit long deadline (e.g. the delete-ack wait) is applied per-call by
* protocol_receive_status_timed and is unaffected by this setter. */
void protocol_session_set_io_timeout(ProtocolSession* session, int sec);
/* Effective per-message I/O deadline (seconds) for the currently-bound session,
* falling back to the built-in default. Used by the plaintext sendfile path
* which bypasses the protocol send primitive. */
int protocol_get_io_timeout_sec(void);
void* protocol_alloc(size_t size);
void* protocol_realloc(void* ptr, size_t size);
void protocol_session_set_8_bit_output(ProtocolSession* session, bool enabled);