Release v2.26.0 #284

Merged
TapTap merged 210 commits from dev into main 2026-09-18 19:05:52 +02:00
3 changed files with 179 additions and 30 deletions
Showing only changes of commit f8252cf3e7 - Show all commits
+76 -30
View File
@@ -202,6 +202,51 @@ static bool receive_wire_bool(int fd, bool* value) {
return true; return true;
} }
/* Cumulative budget for the strings retained by one received Config (see
* MAX_CONFIG_STRING_BYTES). Config strings are received once per connection
* before authentication and live for its whole lifetime, so the charge is never
* released. */
typedef struct {
unsigned long long used;
} ConfigStringBudget;
/* Charge `bytes` (the retained allocation: string body plus NUL) against the
* aggregate config-string budget. Returns false when the ceiling would be
* exceeded, letting the caller reject the frame with a clear error instead of
* retaining unbounded pre-auth memory. */
static bool config_string_budget_charge(ConfigStringBudget* budget, size_t bytes) {
if ((unsigned long long)bytes > MAX_CONFIG_STRING_BYTES ||
budget->used > MAX_CONFIG_STRING_BYTES - (unsigned long long)bytes) {
log_message(LOG_LEVEL_ERROR, "Config string budget exceeded (%llu + %zu > %llu bytes)",
budget->used, bytes, (unsigned long long)MAX_CONFIG_STRING_BYTES);
return false;
}
budget->used += (unsigned long long)bytes;
return true;
}
static char* config_receive_str(int fd, ConfigStringBudget* budget) {
char* value = receive_str(fd);
if (!value)
return NULL;
if (!config_string_budget_charge(budget, strlen(value) + 1)) {
free(value);
return NULL;
}
return value;
}
static char* config_receive_str_redacted(int fd, ConfigStringBudget* budget) {
char* value = receive_str_redacted(fd);
if (!value)
return NULL;
if (!config_string_budget_charge(budget, strlen(value) + 1)) {
free(value);
return NULL;
}
return value;
}
static bool validate_received_config(const Config* config) { static bool validate_received_config(const Config* config) {
return valid_wire_bool(config->save_to_disk) && valid_wire_bool(config->use_multithreading) && return valid_wire_bool(config->save_to_disk) && valid_wire_bool(config->use_multithreading) &&
valid_wire_bool(config->use_chunk_serialization) && valid_wire_bool(config->use_chunk_serialization) &&
@@ -257,7 +302,7 @@ static bool validate_received_config(const Config* config) {
config->delta_block_size <= DELTA_BLOCK_SIZE_MAX && config->delta_block_size <= DELTA_BLOCK_SIZE_MAX &&
config->delta_max_file_size <= DELTA_MAX_FILE_SIZE && config->modify_window >= 0 && 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->skip_compress_count >= 0 &&
config->skip_compress_count <= 10000 && config->max_alloc > 0 && config->skip_compress_count <= MAX_SKIP_COMPRESS_SUFFIXES && config->max_alloc > 0 &&
(!config->chmod_spec || !*config->chmod_spec || (!config->chmod_spec || !*config->chmod_spec ||
chmod_apply(0, config->chmod_spec, &(mode_t){0})) && chmod_apply(0, config->chmod_spec, &(mode_t){0})) &&
/* The received --iconv CONVERT_SPEC is untrusted input that drives /* The received --iconv CONVERT_SPEC is untrusted input that drives
@@ -819,7 +864,7 @@ static bool send_checksum_options(int fd, const Config* c) {
send_n_data(fd, &c->checksum_seed, sizeof(c->checksum_seed)); send_n_data(fd, &c->checksum_seed, sizeof(c->checksum_seed));
} }
static bool receive_core_fields(int fd, Config* c) { static bool receive_core_fields(int fd, Config* c, ConfigStringBudget* budget) {
int value; int value;
if (!receive_wire_bool(fd, &c->eight_bit_output)) if (!receive_wire_bool(fd, &c->eight_bit_output))
return false; return false;
@@ -829,8 +874,8 @@ static bool receive_core_fields(int fd, Config* c) {
if (c->max_alloc > MAX_SERVER_ALLOC) if (c->max_alloc > MAX_SERVER_ALLOC)
c->max_alloc = MAX_SERVER_ALLOC; c->max_alloc = MAX_SERVER_ALLOC;
protocol_session_set_max_alloc(NULL, c->max_alloc); protocol_session_set_max_alloc(NULL, c->max_alloc);
c->send_directory = receive_str(fd); c->send_directory = config_receive_str(fd, budget);
c->receive_root_directory = receive_str(fd); c->receive_root_directory = config_receive_str(fd, budget);
if (!c->send_directory || !c->receive_root_directory) if (!c->send_directory || !c->receive_root_directory)
return false; return false;
if (!receive_wire_bool(fd, &c->save_to_disk) || !receive_wire_bool(fd, &c->use_multithreading) || if (!receive_wire_bool(fd, &c->save_to_disk) || !receive_wire_bool(fd, &c->use_multithreading) ||
@@ -863,10 +908,10 @@ static bool receive_delta_fields(int fd, Config* c) {
receive_n_data(fd, &c->delta_max_file_size, sizeof(unsigned long long)); receive_n_data(fd, &c->delta_max_file_size, sizeof(unsigned long long));
} }
static bool receive_file_options(int fd, Config* c) { static bool receive_file_options(int fd, Config* c, ConfigStringBudget* budget) {
if (!receive_wire_bool(fd, &c->backup)) if (!receive_wire_bool(fd, &c->backup))
return false; return false;
char* backup_dir = receive_str(fd); char* backup_dir = config_receive_str(fd, budget);
if (!backup_dir) if (!backup_dir)
return false; return false;
if (*backup_dir != '\0') { if (*backup_dir != '\0') {
@@ -920,8 +965,8 @@ static bool receive_selection_options(int fd, Config* c) {
return receive_wire_bool(fd, &c->delete_delay); return receive_wire_bool(fd, &c->delete_delay);
} }
static bool receive_resume_options(int fd, Config* c) { static bool receive_resume_options(int fd, Config* c, ConfigStringBudget* budget) {
char* temp_dir = receive_str(fd); char* temp_dir = config_receive_str(fd, budget);
if (!temp_dir) if (!temp_dir)
return false; return false;
if (*temp_dir != '\0') { if (*temp_dir != '\0') {
@@ -935,7 +980,7 @@ static bool receive_resume_options(int fd, Config* c) {
string for "unset". Canonicalize the empty wire value back to NULL so string for "unset". Canonicalize the empty wire value back to NULL so
receivers observe exactly what the client configured (plain --backup, for receivers observe exactly what the client configured (plain --backup, for
example, must not look like --backup-dir ""). */ example, must not look like --backup-dir ""). */
char* partial_dir = receive_str(fd); char* partial_dir = config_receive_str(fd, budget);
if (!partial_dir) if (!partial_dir)
return false; return false;
if (*partial_dir != '\0') { if (*partial_dir != '\0') {
@@ -943,7 +988,7 @@ static bool receive_resume_options(int fd, Config* c) {
} else { } else {
free(partial_dir); free(partial_dir);
} }
char* suffix = receive_str(fd); char* suffix = config_receive_str(fd, budget);
if (!suffix) if (!suffix)
return false; return false;
if (*suffix != '\0') { if (*suffix != '\0') {
@@ -957,20 +1002,20 @@ static bool receive_resume_options(int fd, Config* c) {
return false; return false;
if (!receive_n_data(fd, &c->modify_window, sizeof(c->modify_window))) if (!receive_n_data(fd, &c->modify_window, sizeof(c->modify_window)))
return false; return false;
c->compress_choice = receive_str(fd); c->compress_choice = config_receive_str(fd, budget);
if (!c->compress_choice) if (!c->compress_choice)
return false; return false;
c->chmod_spec = receive_str(fd); c->chmod_spec = config_receive_str(fd, budget);
if (!c->chmod_spec || !receive_wire_bool(fd, &c->skip_compress_set) || if (!c->chmod_spec || !receive_wire_bool(fd, &c->skip_compress_set) ||
!receive_int(fd, &c->skip_compress_count) || c->skip_compress_count < 0 || !receive_int(fd, &c->skip_compress_count) || c->skip_compress_count < 0 ||
c->skip_compress_count > 10000) c->skip_compress_count > MAX_SKIP_COMPRESS_SUFFIXES)
return false; return false;
if (c->skip_compress_count > 0) { if (c->skip_compress_count > 0) {
c->skip_compress_suffixes = calloc((size_t)c->skip_compress_count, sizeof(char*)); c->skip_compress_suffixes = calloc((size_t)c->skip_compress_count, sizeof(char*));
if (!c->skip_compress_suffixes) if (!c->skip_compress_suffixes)
return false; return false;
for (int i = 0; i < c->skip_compress_count; i++) { for (int i = 0; i < c->skip_compress_count; i++) {
c->skip_compress_suffixes[i] = receive_str(fd); c->skip_compress_suffixes[i] = config_receive_str(fd, budget);
if (!c->skip_compress_suffixes[i]) if (!c->skip_compress_suffixes[i])
return false; return false;
} }
@@ -978,7 +1023,7 @@ static bool receive_resume_options(int fd, Config* c) {
return true; return true;
} }
static bool receive_basis_options(int fd, Config* c) { static bool receive_basis_options(int fd, Config* c, ConfigStringBudget* budget) {
int count; int count;
if (!receive_int(fd, &count)) if (!receive_int(fd, &count))
return false; return false;
@@ -988,7 +1033,7 @@ static bool receive_basis_options(int fd, Config* c) {
int type; int type;
if (!receive_int(fd, &type) || type <= BASIS_DEST_NONE || type > BASIS_DEST_LINK) if (!receive_int(fd, &type) || type <= BASIS_DEST_NONE || type > BASIS_DEST_LINK)
return false; return false;
char* path = receive_str(fd); char* path = config_receive_str(fd, budget);
if (!path) if (!path)
return false; return false;
/* config_basis_append validates and canonicalizes the path; a rejected /* config_basis_append validates and canonicalizes the path; a rejected
@@ -1119,8 +1164,8 @@ static bool send_daemon_module(int fd, const Config* c) {
return send_str(fd, c->module ? c->module : ""); return send_str(fd, c->module ? c->module : "");
} }
static bool receive_daemon_module(int fd, Config* c) { static bool receive_daemon_module(int fd, Config* c, ConfigStringBudget* budget) {
char* module = receive_str(fd); char* module = config_receive_str(fd, budget);
if (!module) if (!module)
return false; return false;
/* Guard against a hostile client flooding the log with an over-long module /* Guard against a hostile client flooding the log with an over-long module
@@ -1155,14 +1200,14 @@ static bool send_daemon_auth(int fd, const Config* c) {
return send_str_redacted(fd, c->auth_user); return send_str_redacted(fd, c->auth_user);
} }
static bool receive_daemon_auth(int fd, Config* c) { static bool receive_daemon_auth(int fd, Config* c, ConfigStringBudget* budget) {
int present; int present;
if (!receive_int(fd, &present) || !valid_wire_bool(present)) if (!receive_int(fd, &present) || !valid_wire_bool(present))
return false; return false;
if (!present) if (!present)
return true; return true;
/* Redacted receive: never log the incoming username body. */ /* Redacted receive: never log the incoming username body. */
char* user = receive_str_redacted(fd); char* user = config_receive_str_redacted(fd, budget);
if (!user) if (!user)
return false; return false;
if (!credentials_username_valid(user)) { if (!credentials_username_valid(user)) {
@@ -1272,8 +1317,8 @@ static bool send_iconv_spec(int fd, const Config* c) {
return send_str(fd, c->iconv_spec ? c->iconv_spec : ""); return send_str(fd, c->iconv_spec ? c->iconv_spec : "");
} }
static bool receive_iconv_spec(int fd, Config* c) { static bool receive_iconv_spec(int fd, Config* c, ConfigStringBudget* budget) {
char* spec = receive_str(fd); char* spec = config_receive_str(fd, budget);
if (!spec) if (!spec)
return false; return false;
if (*spec == '\0') { if (*spec == '\0') {
@@ -1376,8 +1421,9 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
Config* config = config_create(); Config* config = config_create();
if (!config) if (!config)
return NULL; return NULL;
ConfigStringBudget budget = {0};
free(config->version); free(config->version);
config->version = receive_str(file_descriptor); config->version = config_receive_str(file_descriptor, &budget);
if (!config->version) if (!config->version)
goto error; goto error;
if (strcmp(config->version, PROTOCOL_VERSION) != 0) { if (strcmp(config->version, PROTOCOL_VERSION) != 0) {
@@ -1388,21 +1434,21 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
send_status(file_descriptor, STATUS_ERROR); send_status(file_descriptor, STATUS_ERROR);
goto error; goto error;
} }
if (!receive_core_fields(file_descriptor, config) || if (!receive_core_fields(file_descriptor, config, &budget) ||
!receive_delta_fields(file_descriptor, config) || !receive_delta_fields(file_descriptor, config) ||
!receive_file_options(file_descriptor, config) || !receive_file_options(file_descriptor, config, &budget) ||
!receive_selection_options(file_descriptor, config) || !receive_selection_options(file_descriptor, config) ||
!receive_resume_options(file_descriptor, config) || !receive_resume_options(file_descriptor, config, &budget) ||
!receive_basis_options(file_descriptor, config) || !receive_basis_options(file_descriptor, config, &budget) ||
!receive_fuzzy_option(file_descriptor, config) || !receive_fuzzy_option(file_descriptor, config) ||
!receive_checksum_options(file_descriptor, config) || !receive_checksum_options(file_descriptor, config) ||
!receive_identity_options(file_descriptor, config) || !receive_identity_options(file_descriptor, config) ||
!receive_metadata_times_options(file_descriptor, config) || !receive_metadata_times_options(file_descriptor, config) ||
!receive_symlink_trust_options(file_descriptor, config) || !receive_symlink_trust_options(file_descriptor, config) ||
!receive_phase4_xattr_options(file_descriptor, config) || !receive_phase4_xattr_options(file_descriptor, config) ||
!receive_daemon_module(file_descriptor, config) || !receive_daemon_module(file_descriptor, config, &budget) ||
!receive_daemon_auth(file_descriptor, config) || !receive_daemon_auth(file_descriptor, config, &budget) ||
!receive_iconv_spec(file_descriptor, config) || !receive_iconv_spec(file_descriptor, config, &budget) ||
!receive_privilege_options(file_descriptor, config) || !receive_privilege_options(file_descriptor, config) ||
!receive_copy_as_options(file_descriptor, config)) !receive_copy_as_options(file_descriptor, config))
goto error; goto error;
+18
View File
@@ -643,6 +643,24 @@ typedef struct Config {
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */ /* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
#define MAX_BASIS_DIRS 64 #define MAX_BASIS_DIRS 64
/* Upper bound on the number of --skip-compress suffixes accepted from the wire.
* Each suffix is an independent wire string (up to MAX_STRING_SIZE = 64 KiB), so
* without this a hostile pre-auth client could otherwise retain
* skip_count * MAX_STRING_SIZE bytes on the server before authentication; 256
* covers any realistic suffix list while keeping the worst case small. */
#define MAX_SKIP_COMPRESS_SUFFIXES 256
/* Aggregate ceiling on the bytes retained by ALL strings in one received config
* frame (version, send/receive roots, backup/temp/partial/suffix, compression
* choice, chmod spec, skip-compress suffixes, basis paths, module, auth user,
* iconv spec, ...). The config frame is parsed BEFORE authentication and every
* one of these strings lives for the whole connection, so this cumulative
* (never released) budget bounds the pre-auth memory a single connection can
* pin. MAX_SKIP_COMPRESS_SUFFIXES / MAX_BASIS_DIRS bound the individual
* repeatable counts; this budget bounds their product and any single oversized
* field. */
#define MAX_CONFIG_STRING_BYTES (1ULL * 1024 * 1024)
/* Identity-mapping sentinels and bounds (see identity.h for semantics). /* Identity-mapping sentinels and bounds (see identity.h for semantics).
* IDENTITY_MATCH_ANY is a usermap/groupmap FROM '*' (matches any id); * IDENTITY_MATCH_ANY is a usermap/groupmap FROM '*' (matches any id);
* IDENTITY_CURRENT is a chown / map TO '*' (resolve to the receiver's current * IDENTITY_CURRENT is a chown / map TO '*' (resolve to the receiver's current
+85
View File
@@ -6,6 +6,7 @@
#include "queue.h" #include "queue.h"
#include "test_utils.h" #include "test_utils.h"
#include "utils.h" #include "utils.h"
#include <signal.h>
#include <stdlib.h> #include <stdlib.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <string.h> #include <string.h>
@@ -1846,6 +1847,89 @@ static void test_config_receive_rejects_copy_as_without_metadata() {
config_delete(c); config_delete(c);
} }
/* Like roundtrip_config_ok, but the parent is the RECEIVER so the frame can be
rejected MID-way, before the sender finishes writing it. The sender child
ignores SIGPIPE so the receiver closing early cannot kill it; the parent
waits for the child to exit after observing the rejection. */
static bool roundtrip_config_rejected(const Config* send_cfg) {
int p[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, p) != 0)
return false;
pid_t pid = fork();
if (pid == 0) {
(void)signal(SIGPIPE, SIG_IGN);
close(p[0]);
io_set_fds(p[1], p[1]);
config_send(p[1], send_cfg);
close(p[1]);
_exit(0);
}
close(p[1]);
io_set_fds(p[0], p[0]);
Config* recv = config_receive(p[0]);
bool rejected = recv == NULL;
config_delete(recv);
close(p[0]);
int status;
waitpid(pid, &status, 0);
return rejected;
}
/* Build a Config with `count` --skip-compress suffixes, each `suffix_len` bytes
long, for the pre-auth config-string budget tests. */
static Config* make_skip_compress_config(int count, size_t suffix_len) {
Config* c = config_create();
if (!c)
return NULL;
c->send_directory = str_dup("/src");
c->receive_root_directory = str_dup("/dst");
c->skip_compress_set = true;
c->skip_compress_count = count;
c->skip_compress_suffixes = calloc((size_t)count, sizeof(char*));
if (!c->skip_compress_suffixes) {
config_delete(c);
return NULL;
}
char* suffix = malloc(suffix_len + 1);
if (!suffix) {
config_delete(c);
return NULL;
}
memset(suffix, 'x', suffix_len);
suffix[suffix_len] = '\0';
for (int i = 0; i < count; i++)
c->skip_compress_suffixes[i] = str_dup(suffix);
free(suffix);
return c;
}
/* Pre-auth memory bound: one connection must not retain unbounded config
strings. An over-limit --skip-compress count is refused, and even an
in-range count cannot exceed the aggregate per-connection string budget. */
static void test_config_receive_rejects_oversized_string_budget() {
if (is_running_under_valgrind())
return;
/* Exactly MAX_SKIP_COMPRESS_SUFFIXES tiny suffixes are accepted. */
Config* ok = make_skip_compress_config(MAX_SKIP_COMPRESS_SUFFIXES, 1);
EXPECT_NOT_NULL(ok);
EXPECT_TRUE(roundtrip_config_ok(ok));
config_delete(ok);
/* One suffix over the count cap is rejected before any suffix is read. */
Config* over_count = make_skip_compress_config(MAX_SKIP_COMPRESS_SUFFIXES + 1, 1);
EXPECT_NOT_NULL(over_count);
EXPECT_TRUE(roundtrip_config_rejected(over_count));
config_delete(over_count);
/* In-range count, but the strings together exceed MAX_CONFIG_STRING_BYTES
(64 suffixes * ~64 KiB > 1 MiB), so the aggregate budget rejects it. */
Config* over_bytes = make_skip_compress_config(64, MAX_STRING_SIZE - 1);
EXPECT_NOT_NULL(over_bytes);
EXPECT_TRUE(roundtrip_config_rejected(over_bytes));
config_delete(over_bytes);
}
/* identity_copy_as_refused() is the pure, pre-snapshot refusal predicate: a /* identity_copy_as_refused() is the pure, pre-snapshot refusal predicate: a
--copy-as is refused when the receiver is not root OR the effective super --copy-as is refused when the receiver is not root OR the effective super
mode is OFF (an operator veto), and never when --copy-as is unset. */ mode is OFF (an operator veto), and never when --copy-as is unset. */
@@ -2009,6 +2093,7 @@ void test_config() {
test_config_copy_as_wire_roundtrip(); test_config_copy_as_wire_roundtrip();
test_config_receive_rejects_negative_copy_as(); test_config_receive_rejects_negative_copy_as();
test_config_receive_rejects_copy_as_without_metadata(); test_config_receive_rejects_copy_as_without_metadata();
test_config_receive_rejects_oversized_string_budget();
test_config_receive_with_validate_rejects(); test_config_receive_with_validate_rejects();
} }
test_identity_copy_as_refused(); test_identity_copy_as_refused();