fix(parity): receiver temp-dir confinement, server I/O floor, delete budget
Address review findings on feat/rsync-parity: - confine --temp-dir below the receive root (reject absolute/.. like backup-dir/partial-dir); keep EXDEV non-atomic fallback - floor server session I/O deadlines at SERVER_IO_TIMEOUT_SEC (60s) and install it on the socket layer at startup (slow-loris) - charge each --delete-missing-args directory removal once and clamp the extras-walk remaining budget so it can never underflow past --max-delete - normalize --compress-choice=auto to zstd client-side and accept it on receive so auto transfers no longer fail - map received --max-alloc=0 to MAX_SERVER_ALLOC (receive path only) - zero File.dest_state; include log-file-format in report_dest_info; add STATUS_DELETE_LIMIT name; recognize --skip-compress as a separate-value option; OOM-guard send_list_only root entry; drop the dead -M= branch; record the bare relative protected prefix for -R size-prunes in both scanners; refresh delete-manifest comment - pin the rsync tarball sha256 and bump integrator image to v11 Tests: temp-dir rejection/relative/cross-device, server timeout floor, delete-missing dir budget regression, compress-choice=auto e2e, max-alloc=0 receive mapping, dest_state, report_dest_info modes, skip-compress dash value, -M short forms, -R root size-prune mirror protection (rsync 3.4.1 confirmed).
This commit is contained in:
+14
-13
@@ -132,16 +132,20 @@ static int set_positive_int_option(int* dest, const char* value, const char* opt
|
||||
* zstd choice; any other rsync choice is rejected by name instead of being
|
||||
* silently accepted and ignored. */
|
||||
static int set_compression_choice(Config* config, const char* value) {
|
||||
if (strcmp(value, "zstd") != 0 && strcmp(value, "none") != 0 && strcmp(value, "auto") != 0) {
|
||||
/* rsync's "auto" is normalized to the canonical "zstd" at parse time (like
|
||||
--checksum-choice=auto), so the value that crosses the wire is always one
|
||||
the receiver accepts. */
|
||||
const char* canonical = strcmp(value, "auto") == 0 ? "zstd" : value;
|
||||
if (strcmp(canonical, "zstd") != 0 && strcmp(canonical, "none") != 0) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--compress-choice '%s' is not implemented; FastSync supports zstd, none or auto "
|
||||
"(rsync's lz4/zlib/zlibx are rejected, never silently ignored)",
|
||||
value);
|
||||
return -1;
|
||||
}
|
||||
if (set_string_option(&config->compress_choice, value, "--compress-choice") != 0)
|
||||
if (set_string_option(&config->compress_choice, canonical, "--compress-choice") != 0)
|
||||
return -1;
|
||||
config->use_compression = strcmp(value, "none") != 0;
|
||||
config->use_compression = strcmp(canonical, "none") != 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1992,11 +1996,6 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
if (strncmp(arg, "-M=", 3) == 0) {
|
||||
if (config_add_remote_option(config, arg + 3, "-M") != 0)
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "--remote-option", "-M")) {
|
||||
if (ctx->i + 1 >= ctx->argc) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for --remote-option");
|
||||
@@ -2271,10 +2270,12 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
|
||||
* --no-xattrs/--no-acls negation) so the sender's wire gate always matches
|
||||
* the flags the receiver will recompute from the received config. */
|
||||
config->use_xattrs = config->preserve_acls || config->preserve_xattrs;
|
||||
/* Output parity: -i/--itemize-changes and --out-format need the pre-transfer
|
||||
* destination snapshot (new vs modified and which attributes differ), so ask
|
||||
* the receiver to report it on every per-file check. This is a wire field. */
|
||||
config->report_dest_info = config->itemize_changes || config->out_format != NULL;
|
||||
/* Output parity: -i/--itemize-changes, --out-format and --log-file-format
|
||||
* need the pre-transfer destination snapshot (new vs modified and which
|
||||
* attributes differ), so ask the receiver to report it on every per-file
|
||||
* check. This is a wire field. */
|
||||
config->report_dest_info = config->itemize_changes || config->out_format != NULL ||
|
||||
(config->log_file != NULL && config->log_file_format != NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2306,7 +2307,7 @@ static bool cli_long_takes_separate_value(const char* arg) {
|
||||
"--checksum-choice", "--cc", "--checksum-seed", "--sockopts",
|
||||
"--remote-option", "--compare-dest", "--copy-dest", "--link-dest",
|
||||
"--usermap", "--groupmap", "--chown", "--copy-as",
|
||||
"--outbuf", "--debug", "--info",
|
||||
"--outbuf", "--debug", "--info", "--skip-compress",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(extra) / sizeof(extra[0]); i++)
|
||||
if (strcmp(arg, extra[i]) == 0)
|
||||
|
||||
+15
-10
@@ -908,8 +908,10 @@ static int send_list_only(const Config* config) {
|
||||
entries = calloc(capacity, sizeof(ListEntry));
|
||||
if (entries == NULL) {
|
||||
oom = true;
|
||||
} else if ((entries[0].name = str_dup("")) == NULL) {
|
||||
/* A NULL name would be dereferenced by qsort/render: fail the listing. */
|
||||
oom = true;
|
||||
} else {
|
||||
entries[0].name = str_dup("");
|
||||
entries[0].mode = st.st_mode;
|
||||
entries[0].mtime = st.st_mtime;
|
||||
entries[0].mtime_nsec = st.st_mtim.tv_nsec;
|
||||
@@ -1015,15 +1017,18 @@ static int send_list_only(const Config* config) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Send the delete manifest (keep-set paths plus the protected excluded
|
||||
prefixes and the --delete-missing-args exact-delete paths) to the server.
|
||||
Returns 0 on success, -1 on failure. When --delete-excluded is given
|
||||
`protected` is empty: excluded destination mirrors are then ordinary extras
|
||||
and are removed. When --delete-missing-args is active `missing_args` holds
|
||||
the destination mirrors of missing --files-from entries: each is an explicit
|
||||
receiver-side deletion request, independent of the extras walk. A NULL
|
||||
keep-set / protected / missing list transmits an empty section. All three
|
||||
sections are unbounded on the sender; the receiver enforces
|
||||
/* Send the delete manifest to the server. Returns 0 on success, -1 on
|
||||
failure. It carries FOUR sections: the keep-set paths, the protected
|
||||
excluded prefixes, the --delete-missing-args exact-delete paths, and the
|
||||
destination-relative directories the sender synchronized this run.
|
||||
When --delete-excluded is given `protected` is empty: excluded destination
|
||||
mirrors are then ordinary extras and are removed. When
|
||||
--delete-missing-args is active `missing_args` holds the destination mirrors
|
||||
of missing --files-from entries: each is an explicit receiver-side deletion
|
||||
request, independent of the extras walk. `synced_dirs` confines the extras
|
||||
walk to entries directly inside a synchronized directory. A NULL
|
||||
keep-set / protected / missing / dirs list transmits an empty section. All
|
||||
four sections are unbounded on the sender; the receiver enforces
|
||||
MAX_MANIFEST_ENTRIES per section and a single MAX_MANIFEST_BYTES budget
|
||||
shared across the sections, rejecting (with STATUS_ERROR) an over-budget
|
||||
frame. A heavily filtered source whose exclusion list is large therefore
|
||||
|
||||
+27
-16
@@ -1120,18 +1120,23 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
||||
if (inspection == 0) {
|
||||
/* A user-selection exclude protects its destination mirror from --delete
|
||||
unless --delete-excluded; a size prune is always protected. Other
|
||||
skips (unreadable, symlink policy) protect nothing. */
|
||||
skips (unreadable, symlink policy) protect nothing. Under -R +
|
||||
--files-from the protected prefix must be the entry's bare relative
|
||||
wire path, not its source path (which would not match the destination
|
||||
layout and would leave the mirror deletable). */
|
||||
if (inspected.excluded) {
|
||||
char* abs_path = path_cat(scanner->current_path, entry->d_name);
|
||||
if (!abs_path) {
|
||||
char* protected_path = scanner->relative_mode
|
||||
? child_rel_path(scanner->current_rel, entry->d_name)
|
||||
: path_cat(scanner->current_path, entry->d_name);
|
||||
if (!protected_path) {
|
||||
scanner->failed = true;
|
||||
break;
|
||||
}
|
||||
if (inspected.size_excluded)
|
||||
scanner_record_size_skipped(scanner, abs_path);
|
||||
scanner_record_size_skipped(scanner, protected_path);
|
||||
else
|
||||
scanner_record_excluded(scanner, abs_path);
|
||||
free(abs_path);
|
||||
scanner_record_excluded(scanner, protected_path);
|
||||
free(protected_path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -1510,17 +1515,23 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
|
||||
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 same-named
|
||||
wire path (at the root the bare name is the wire path in every
|
||||
layout). */
|
||||
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))
|
||||
/* 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;
|
||||
free(abs_path);
|
||||
} else {
|
||||
char* abs_path = path_cat(root_directory, entry->d_name);
|
||||
if (!abs_path) {
|
||||
ps->failed = true;
|
||||
} else {
|
||||
const char* rel = *abs_path == '/' ? abs_path + 1 : abs_path;
|
||||
if (!excluded_sink_append(sink, options->excluded_mutex, rel))
|
||||
ps->failed = true;
|
||||
free(abs_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
+11
-5
@@ -752,10 +752,11 @@ void handler(int file_descriptor) {
|
||||
protocol_set_8_bit_output(config->eight_bit_output);
|
||||
/* 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);
|
||||
* has no --timeout CLI and defaults it to 0). A client's --timeout tightens
|
||||
* only that client's own protocol I/O; the server floors its own deadline at
|
||||
* SERVER_IO_TIMEOUT_SEC so a silent peer can never hold a session slot
|
||||
* forever (the socket layer gets the same floor at startup). */
|
||||
protocol_session_set_io_timeout(&session, protocol_server_io_timeout_sec(config->timeout));
|
||||
const char* authorized_root = utils_get_authorized_root_path();
|
||||
if (!authorized_root) {
|
||||
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
||||
@@ -904,7 +905,8 @@ void handler(int file_descriptor) {
|
||||
goto done;
|
||||
}
|
||||
protocol_session_set_max_alloc(&context->session, config->max_alloc);
|
||||
protocol_session_set_io_timeout(&context->session, config->timeout);
|
||||
protocol_session_set_io_timeout(&context->session,
|
||||
protocol_server_io_timeout_sec(config->timeout));
|
||||
atomic_store(&context->session.total_allocated_bytes,
|
||||
atomic_load(&session.total_allocated_bytes));
|
||||
pipeline_context_receiver_set_queue_byte_limit(context, RECEIVER_QUEUE_MAX_BYTES);
|
||||
@@ -1220,6 +1222,10 @@ int main(int argc, char* argv[]) {
|
||||
server_iconv_spec = opts.iconv_spec;
|
||||
signal(SIGINT, cleanup);
|
||||
signal(SIGTERM, cleanup);
|
||||
/* 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. */
|
||||
|
||||
+22
-8
@@ -46,9 +46,11 @@ static void config_set_defaults(Config* config) {
|
||||
config->server_port_set = false;
|
||||
config->server_host_set = false;
|
||||
/* rsync defaults: --timeout=0 (I/O timeouts disabled) and --contimeout=60.
|
||||
* A value of 0 disables the deadline on both the socket layer
|
||||
* A value of 0 disables the client's own deadline on both the socket layer
|
||||
* (tcp_set_timeouts) and the protocol layer
|
||||
* (protocol_session_set_io_timeout); a positive value sets it. */
|
||||
* (protocol_session_set_io_timeout); a positive value sets it. A server
|
||||
* session floors the deadline at SERVER_IO_TIMEOUT_SEC so 0 can never hold a
|
||||
* connection open forever. */
|
||||
config->timeout = 0;
|
||||
config->contimeout = 60;
|
||||
config->quiet = false;
|
||||
@@ -207,7 +209,8 @@ static bool validate_received_config(const Config* config) {
|
||||
config->delta_block_size >= DELTA_BLOCK_SIZE_MIN &&
|
||||
config->delta_block_size <= DELTA_BLOCK_SIZE_MAX &&
|
||||
config->delta_max_file_size <= DELTA_MAX_FILE_SIZE && config->modify_window >= 0 &&
|
||||
config->max_delete >= -1 && config->skip_compress_count >= 0 &&
|
||||
config->max_delete >= -1 && config->max_alloc <= MAX_SERVER_ALLOC &&
|
||||
config->skip_compress_count >= 0 &&
|
||||
config->skip_compress_count <= MAX_SKIP_COMPRESS_SUFFIXES &&
|
||||
(!config->chmod_spec || !*config->chmod_spec ||
|
||||
chmod_apply(0, config->chmod_spec, &(mode_t){0})) &&
|
||||
@@ -788,13 +791,14 @@ void config_delete(Config* config) {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/* --max-alloc: raw 64-bit value, clamped server-side and installed as the
|
||||
* session allocation ceiling. Zero means "no alloc limit" (rsync's
|
||||
* --max-alloc=0) and is passed through; a non-zero value is clamped to the
|
||||
* server's own ceiling. */
|
||||
* session allocation ceiling. A received 0 is rsync's "no alloc limit"; on the
|
||||
* receive path it is mapped to the server ceiling so a client can never disable
|
||||
* it (client-side 0 remains unlimited). Any value above the ceiling is clamped
|
||||
* to it. */
|
||||
static bool config_receive_max_alloc(int fd, unsigned long long* value) {
|
||||
if (!receive_n_data(fd, value, sizeof(*value)))
|
||||
return false;
|
||||
if (*value > MAX_SERVER_ALLOC)
|
||||
if (*value == 0 || *value > MAX_SERVER_ALLOC)
|
||||
*value = MAX_SERVER_ALLOC;
|
||||
protocol_session_set_max_alloc(NULL, *value);
|
||||
return true;
|
||||
@@ -1362,7 +1366,8 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
|
||||
!receive_output_options(file_descriptor, config, &budget))
|
||||
goto error;
|
||||
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 &&
|
||||
strcmp(config->compress_choice, "none") != 0) {
|
||||
strcmp(config->compress_choice, "none") != 0 &&
|
||||
strcmp(config->compress_choice, "auto") != 0) {
|
||||
char* escaped_choice = output_escape(config->compress_choice, config->eight_bit_output);
|
||||
log_message(LOG_LEVEL_ERROR, "Unsupported compression choice: %s",
|
||||
escaped_choice ? escaped_choice : "<allocation failed>");
|
||||
@@ -1373,6 +1378,15 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
|
||||
free(escaped_choice);
|
||||
goto error;
|
||||
}
|
||||
/* Defensive: an older/hostile client may still send "auto"; canonicalize it
|
||||
to zstd (its effective choice) so the stored value is always concrete. */
|
||||
if (strcmp(config->compress_choice, "auto") == 0) {
|
||||
char* canonical = str_dup("zstd");
|
||||
if (!canonical)
|
||||
goto error;
|
||||
free(config->compress_choice);
|
||||
config->compress_choice = canonical;
|
||||
}
|
||||
if (!validate_received_config(config)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid configuration received from client");
|
||||
send_error_detail(file_descriptor, "invalid configuration received from client");
|
||||
|
||||
+4
-3
@@ -328,9 +328,10 @@ typedef struct Config {
|
||||
char* tls_key;
|
||||
char* tls_ca;
|
||||
/* --timeout: per-message I/O deadline in seconds. 0 (rsync's default)
|
||||
* disables the deadline entirely on both the socket layer and the protocol
|
||||
* layer; a positive value sets it. See protocol_session_set_io_timeout and
|
||||
* tcp_set_timeouts. */
|
||||
* disables the deadline entirely on the client's own socket and protocol
|
||||
* layers; a positive value sets it. A server session never inherits the
|
||||
* disabled value: it applies the SERVER_IO_TIMEOUT_SEC floor (see
|
||||
* protocol_server_io_timeout_sec and tcp_set_timeouts). */
|
||||
int timeout;
|
||||
/* --contimeout: connect()/accept timeout in seconds (rsync's default 60);
|
||||
* 0 disables it. Transport layer only. */
|
||||
|
||||
@@ -182,6 +182,7 @@ File* file_create(const char* path) {
|
||||
file->rdev_major = 0;
|
||||
file->rdev_minor = 0;
|
||||
file->xattrs = NULL;
|
||||
file->dest_state = (OutputDestState){0};
|
||||
return file;
|
||||
}
|
||||
|
||||
|
||||
+41
-27
@@ -295,12 +295,17 @@ static FileSaveResult file_save_hardlink_sibling(const char* root_directory, con
|
||||
free(destination_path);
|
||||
return absent_result;
|
||||
}
|
||||
/* Resolve a relative --temp-dir against the destination root, exactly as the
|
||||
* primary save path does; an absolute one is used verbatim. */
|
||||
/* Resolve a relative --temp-dir under the destination root, exactly as the
|
||||
* primary save path does; an absolute or `..`-escaping value is rejected. */
|
||||
char* resolved_temp = NULL;
|
||||
if (cfg->temp_dir) {
|
||||
resolved_temp =
|
||||
cfg->temp_dir[0] == '/' ? str_dup(cfg->temp_dir) : path_cat(root_directory, cfg->temp_dir);
|
||||
if (cfg->temp_dir[0] == '/' || has_path_traversal(cfg->temp_dir)) {
|
||||
free(content);
|
||||
free(first_disk);
|
||||
free(destination_path);
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
resolved_temp = path_cat(root_directory, cfg->temp_dir);
|
||||
if (!resolved_temp) {
|
||||
free(content);
|
||||
free(first_disk);
|
||||
@@ -772,15 +777,16 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
return file_save_hardlink_sibling(root_directory, file, config);
|
||||
}
|
||||
|
||||
/* These options arrive from the client. --backup-dir and --partial-dir are
|
||||
names below the server root, never independent filesystem roots: an
|
||||
absolute or `..`-escaping value is rejected outright. --temp-dir is
|
||||
deliberately NOT confined: rsync accepts any temp dir (absolute, or
|
||||
relative to the destination root), including one outside the destination
|
||||
tree or on another filesystem, and falls back to a non-atomic copy when
|
||||
the install rename hits EXDEV. */
|
||||
/* These options arrive from the client. --backup-dir, --partial-dir and
|
||||
--temp-dir are names below the server root, never independent filesystem
|
||||
roots: an absolute or `..`-escaping value is rejected outright (rsync's
|
||||
daemon confines temp-dir to the module the same way). A relative temp dir
|
||||
is resolved under the receive root below; if that resolution still lands on
|
||||
a different filesystem than the destination the install falls back to a
|
||||
non-atomic copy (see file_to_disk_secure_impl), never an abort. */
|
||||
if ((backup_dir && (backup_dir[0] == '/' || has_path_traversal(backup_dir))) ||
|
||||
(partial_dir && (partial_dir[0] == '/' || has_path_traversal(partial_dir))))
|
||||
(partial_dir && (partial_dir[0] == '/' || has_path_traversal(partial_dir))) ||
|
||||
(temp_dir && (temp_dir[0] == '/' || has_path_traversal(temp_dir))))
|
||||
return FILE_SAVE_ERROR;
|
||||
if (backup_dir && !(confined_backup = path_cat(root_directory, backup_dir)))
|
||||
return FILE_SAVE_ERROR;
|
||||
@@ -906,20 +912,16 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
|
||||
/* A configured --temp-dir sends the temporary working copy to a scratch
|
||||
directory; the engine then atomically renames the completed file into the
|
||||
final destination directory. rsync resolves a relative temp dir against
|
||||
the destination directory and uses an absolute one verbatim, requiring
|
||||
that it already exist; the engine falls back to a non-atomic copy on
|
||||
EXDEV. The partial-dir flow already keeps its working copy in a separate
|
||||
directory and --inplace writes directly, so neither diverts through the
|
||||
scratch dir (matching rsync, where --inplace/--partial-dir supersede
|
||||
--temp-dir). */
|
||||
final destination directory. A relative temp dir is resolved under the
|
||||
receive root and must already exist (an absolute or `..`-escaping value was
|
||||
rejected above); the engine falls back to a non-atomic copy on EXDEV. The
|
||||
partial-dir flow already keeps its working copy in a separate directory and
|
||||
--inplace writes directly, so neither diverts through the scratch dir
|
||||
(matching rsync, where --inplace/--partial-dir supersede --temp-dir). */
|
||||
char* confined_temp = NULL;
|
||||
bool use_temp_dir = temp_dir != NULL && !inplace && !use_partial_root;
|
||||
if (use_temp_dir) {
|
||||
if (temp_dir[0] == '/')
|
||||
confined_temp = str_dup(temp_dir);
|
||||
else
|
||||
confined_temp = path_cat(root_directory, temp_dir);
|
||||
confined_temp = path_cat(root_directory, temp_dir);
|
||||
if (!confined_temp)
|
||||
goto fail;
|
||||
/* A user-supplied trailing slash would leave the scratch path ending in
|
||||
@@ -3061,8 +3063,15 @@ static bool delete_extras_budgeted(const Config* config, DeleteManifest* manifes
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
size_t remaining =
|
||||
budget->max_delete == SIZE_MAX ? SIZE_MAX : budget->max_delete - budget->deleted;
|
||||
/* Clamp rather than subtract: an accounting bug where deleted already exceeds
|
||||
max_delete must never underflow into an effectively unlimited budget. */
|
||||
size_t remaining;
|
||||
if (budget->max_delete == SIZE_MAX)
|
||||
remaining = SIZE_MAX;
|
||||
else if (budget->deleted >= budget->max_delete)
|
||||
remaining = 0;
|
||||
else
|
||||
remaining = budget->max_delete - budget->deleted;
|
||||
size_t deleted = 0;
|
||||
size_t skipped = 0;
|
||||
DeleteWalkResult result =
|
||||
@@ -3195,7 +3204,11 @@ static bool delete_missing_args_budgeted(const Config* config, DeleteManifest* m
|
||||
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);
|
||||
size_t remaining = budget->max_delete - budget->deleted;
|
||||
/* 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;
|
||||
DeleteWalkResult walk =
|
||||
@@ -3214,7 +3227,8 @@ static bool delete_missing_args_budgeted(const Config* config, DeleteManifest* m
|
||||
budget->limit_hit = true;
|
||||
budget->skipped++;
|
||||
} else if (file_remove_tree_secure(full)) {
|
||||
budget->deleted++;
|
||||
/* The shared `if (removed)` tail charges this directory exactly
|
||||
once; counting it here too would consume two budget units. */
|
||||
removed = true;
|
||||
} else {
|
||||
ok = false;
|
||||
|
||||
@@ -104,6 +104,10 @@ int protocol_get_io_timeout_sec(void) {
|
||||
return session->io_timeout_sec > 0 ? session->io_timeout_sec : 0;
|
||||
}
|
||||
|
||||
int protocol_server_io_timeout_sec(int client_timeout) {
|
||||
return client_timeout > 0 ? client_timeout : SERVER_IO_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;
|
||||
@@ -495,6 +499,8 @@ static const char* status_to_string(Status status) {
|
||||
return "ERROR_DETAIL";
|
||||
case STATUS_DRY_RUN_TRANSFER:
|
||||
return "DRY_RUN_TRANSFER";
|
||||
case STATUS_DELETE_LIMIT:
|
||||
return "DELETE_LIMIT";
|
||||
case STATUS_DEST_INFO:
|
||||
return "DEST_INFO";
|
||||
default:
|
||||
|
||||
+23
-11
@@ -34,6 +34,11 @@
|
||||
#define DEFAULT_MAX_ALLOC (1ULL * 1024 * 1024 * 1024)
|
||||
/* Server policy ceiling for a client-provided allocation limit. */
|
||||
#define MAX_SERVER_ALLOC (256ULL * 1024 * 1024)
|
||||
/* Server-owned floor for the per-message I/O deadline. A client --timeout=0
|
||||
(rsync's default) disables the client's own deadlines, but a server session
|
||||
must never be held open forever by a silent peer (slow-loris), so the server
|
||||
floors the effective deadline at this value. */
|
||||
#define SERVER_IO_TIMEOUT_SEC 60
|
||||
/* Bounded cumulative per-connection receive budget. In-flight wire buffers,
|
||||
decompression buffers and queued (not yet written) file payloads for a
|
||||
connection must stay within this ceiling. */
|
||||
@@ -59,10 +64,12 @@ typedef struct ProtocolSession {
|
||||
bool eight_bit_output;
|
||||
unsigned long long max_alloc;
|
||||
/* Per-session deadline (seconds) applied to every protocol send/receive by
|
||||
* protocol_send_n_data / protocol_receive_n_data. Defaults to the built-in
|
||||
* 60 s window; a value <= 0 falls back to that default. Set from the
|
||||
* negotiated Config->timeout so --timeout is honored by the poll()-driven
|
||||
* protocol I/O, not just the socket SO_RCVTIMEO/SO_SNDTIMEO. */
|
||||
* protocol_send_n_data / protocol_receive_n_data. The initialized default is
|
||||
* the built-in 60 s window; a value <= 0 disables the deadline (rsync's
|
||||
* --timeout=0). Set from the negotiated Config->timeout so --timeout is
|
||||
* honored by the poll()-driven protocol I/O, not just the socket
|
||||
* SO_RCVTIMEO/SO_SNDTIMEO. The server does not propagate a client 0 here: it
|
||||
* installs protocol_server_io_timeout_sec() so its sessions keep a floor. */
|
||||
int io_timeout_sec;
|
||||
} ProtocolSession;
|
||||
|
||||
@@ -189,15 +196,20 @@ void protocol_session_unbind(void);
|
||||
void protocol_session_set_ssl(ProtocolSession* session, SSL* ssl);
|
||||
void protocol_session_set_bwlimit(ProtocolSession* session, unsigned long long bytes_per_sec);
|
||||
void protocol_session_set_max_alloc(ProtocolSession* session, unsigned long long max_alloc);
|
||||
/* Override the per-message send/receive deadline for this session.
|
||||
* `sec` <= 0 restores the built-in 60 s default (used for --timeout=0/unset).
|
||||
* 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. */
|
||||
/* Override the per-message send/receive deadline for this session. The value
|
||||
* is stored verbatim: a positive value sets the deadline, `sec` <= 0 disables
|
||||
* it (rsync's --timeout=0). 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. */
|
||||
/* Effective per-message I/O deadline (seconds) for the currently-bound session.
|
||||
* Zero means the deadline is disabled (rsync's --timeout=0). Used by the
|
||||
* plaintext sendfile path which bypasses the protocol send primitive. */
|
||||
int protocol_get_io_timeout_sec(void);
|
||||
/* The server-side effective deadline for a client-requested timeout: a positive
|
||||
* client value is honored, otherwise the SERVER_IO_TIMEOUT_SEC floor applies so
|
||||
* a silent peer can never hold a session open forever. */
|
||||
int protocol_server_io_timeout_sec(int client_timeout);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user