Merge branch 'fix/audit-refb' into fix/audit-cycle

This commit is contained in:
2026-09-21 21:09:20 +02:00
10 changed files with 45 additions and 65 deletions
+2 -2
View File
@@ -26,9 +26,9 @@ elseif(NOT SANITIZER STREQUAL "none")
endif() endif()
# --- Strict warnings option --- # --- Strict warnings option ---
option(STRICT_WARNINGS "Enable strict warnings (Wextra, Wpedantic, Werror)" OFF) option(STRICT_WARNINGS "Enable strict warnings (Wextra, Wpedantic, Wformat-signedness, Werror)" OFF)
if(STRICT_WARNINGS) if(STRICT_WARNINGS)
add_compile_options(-Wextra -Wpedantic -Werror) add_compile_options(-Wextra -Wpedantic -Wformat-signedness -Werror)
endif() endif()
# --- Coverage option --- # --- Coverage option ---
+1 -1
View File
@@ -3296,7 +3296,7 @@ int main(int argc, char* argv[]) {
exit_code = 1; exit_code = 1;
} }
} else if (config->use_multithreading) { } else if (config->use_multithreading) {
exit_code = send_files_multithreaded(&config); exit_code = send_files_multithreaded(config);
} else { } else {
exit_code = send_files(config); exit_code = send_files(config);
} }
+3 -6
View File
@@ -37,8 +37,6 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#define STREAM_THRESHOLD (64ULL * 1024 * 1024)
/* Aggregate loaded payload bytes the sender may buffer across the loader queue /* Aggregate loaded payload bytes the sender may buffer across the loader queue
and the chunk in flight. Sending one chunk adds up to ~2 * MAX_CHUNK_SIZE of and the chunk in flight. Sending one chunk adds up to ~2 * MAX_CHUNK_SIZE of
transient serialize/compress buffers on top of the queued payloads, so this transient serialize/compress buffers on top of the queued payloads, so this
@@ -1115,7 +1113,7 @@ static Client* connect_transfer_client(const Config* config) {
return NULL; return NULL;
} }
return client_connect_ssh(config->ssh_destination, config->ssh_port, return client_connect_ssh(config->ssh_destination, config->ssh_port,
config->fastsync_server_path, config->old_args, config->rsh_command, config->fastsync_server_path, config->rsh_command,
config->blocking_io, config->remote_options, config->blocking_io, config->remote_options,
config->remote_option_count); config->remote_option_count);
} }
@@ -3617,10 +3615,9 @@ send_fail:
return ret; return ret;
} }
int send_files_multithreaded(Config** config_ptr) { int send_files_multithreaded(Config* config) {
if (!config_ptr || !*config_ptr) if (!config)
return 1; return 1;
Config* config = *config_ptr;
if (config->list_only) if (config->list_only)
return send_list_only(config); return send_list_only(config);
if (config->dry_run) if (config->dry_run)
+1 -1
View File
@@ -22,7 +22,7 @@ void client_set_abort_armed(bool armed);
* never free it, and the caller retains ownership (freeing it with * never free it, and the caller retains ownership (freeing it with
* config_delete() once the call returns). */ * config_delete() once the call returns). */
int send_files(Config* config); int send_files(Config* config);
int send_files_multithreaded(Config** config); int send_files_multithreaded(Config* config);
/* rsync's --ignore-errors deletion gate: with no I/O error during the scan the /* rsync's --ignore-errors deletion gate: with no I/O error during the scan the
* deletion phase always proceeds; with one it is suppressed unless * deletion phase always proceeds; with one it is suppressed unless
* `--ignore-errors` was given. Exposed so the decision can be unit-tested * `--ignore-errors` was given. Exposed so the decision can be unit-tested
+5 -4
View File
@@ -17,8 +17,9 @@
#include "protocol.h" #include "protocol.h"
#include "utils.h" #include "utils.h"
/* Maximum individual file data size within a chunk (64 MB) */ /* Maximum individual file data size within a chunk (64 MB). Distinct from the
#define MAX_FILE_DATA_SIZE (64ULL * 1024 * 1024) * receiver's whole-file MAX_FILE_DATA_SIZE (256 MB) in file_receive.c. */
#define MAX_CHUNK_FILE_DATA_SIZE (64ULL * 1024 * 1024)
#define MAX_FILES_PER_CHUNK 65536U #define MAX_FILES_PER_CHUNK 65536U
/* Reserve `charge` against `session`'s connection budget. This mirrors the /* Reserve `charge` against `session`'s connection budget. This mirrors the
@@ -382,9 +383,9 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
} }
// Reject individual file data larger than the maximum allowed size. // Reject individual file data larger than the maximum allowed size.
if (file_data_size > MAX_FILE_DATA_SIZE) { if (file_data_size > MAX_CHUNK_FILE_DATA_SIZE) {
log_message(LOG_LEVEL_ERROR, "File data size %zu exceeds maximum %llu", file_data_size, log_message(LOG_LEVEL_ERROR, "File data size %zu exceeds maximum %llu", file_data_size,
(unsigned long long)MAX_FILE_DATA_SIZE); (unsigned long long)MAX_CHUNK_FILE_DATA_SIZE);
goto error; goto error;
} }
-5
View File
@@ -26,11 +26,6 @@
#include "protocol.h" #include "protocol.h"
#include "xattr.h" #include "xattr.h"
/* Files larger than this are not loaded whole for transfer (the sender streams
* them); a whole-file digest is computed from the path instead. Kept in sync
* with the sender's streaming threshold. */
#define STREAM_THRESHOLD (64ULL * 1024 * 1024)
static bool write_all(int fd, const void* data, unsigned long long size) { static bool write_all(int fd, const void* data, unsigned long long size) {
const unsigned char* p = data; const unsigned char* p = data;
unsigned long long done = 0; unsigned long long done = 0;
+4
View File
@@ -28,6 +28,10 @@
/* Maximum chunk size (64 MB) — prevents unbounded allocation from the wire */ /* Maximum chunk size (64 MB) — prevents unbounded allocation from the wire */
#define MAX_CHUNK_SIZE (64ULL * 1024 * 1024) #define MAX_CHUNK_SIZE (64ULL * 1024 * 1024)
/* Files larger than this are not kept fully in memory while loading: the
* loader skips them so the sender streams from the path, and file_checksum
* hashes them from disk in bounded buffers instead of forcing a full load. */
#define STREAM_THRESHOLD (64ULL * 1024 * 1024)
#define MAX_MANIFEST_ENTRIES (1024 * 1024) #define MAX_MANIFEST_ENTRIES (1024 * 1024)
/* Aggregate bytes retained by one received deletion manifest. */ /* Aggregate bytes retained by one received deletion manifest. */
#define MAX_MANIFEST_BYTES (16ULL * 1024 * 1024) #define MAX_MANIFEST_BYTES (16ULL * 1024 * 1024)
+5 -7
View File
@@ -87,7 +87,7 @@ static int parse_remote_dest(const char* dest, RemoteDest* r) {
return 0; return 0;
} }
char* ssh_build_remote_command(const char* server_path, bool old_args, char* const* remote_options, char* ssh_build_remote_command(const char* server_path, char* const* remote_options,
int remote_option_count) { int remote_option_count) {
const char* path = server_path ? server_path : "fastsync-server"; const char* path = server_path ? server_path : "fastsync-server";
const char* suffix = " --stdio"; const char* suffix = " --stdio";
@@ -105,9 +105,7 @@ char* ssh_build_remote_command(const char* server_path, bool old_args, char* con
shell word (remote options below reuse the same escaping), then shell word (remote options below reuse the same escaping), then
" --stdio". Quoting the path is the only injection-safe construction: an " --stdio". Quoting the path is the only injection-safe construction: an
unquoted path would carry shell metacharacters straight into the remote unquoted path would carry shell metacharacters straight into the remote
shell command. --old-args is kept for CLI/ABI compatibility but no longer shell command. (rsync's --old-args no longer disables that protection.) */
disables that protection. */
(void)old_args;
size_t quote_count = 0; size_t quote_count = 0;
for (const char* p = path; *p; p++) for (const char* p = path; *p; p++)
if (*p == '\'') if (*p == '\'')
@@ -296,8 +294,8 @@ void ssh_free_client_argv(char** argv) {
} }
Client* client_connect_ssh(const char* destination, int port, const char* server_path, Client* client_connect_ssh(const char* destination, int port, const char* server_path,
bool old_args, const char* rsh_command, bool blocking_io, const char* rsh_command, bool blocking_io, char* const* remote_options,
char* const* remote_options, int remote_option_count) { int remote_option_count) {
RemoteDest r; RemoteDest r;
if (parse_remote_dest(destination, &r) != 0) { if (parse_remote_dest(destination, &r) != 0) {
char* escaped = output_escape(destination, false); char* escaped = output_escape(destination, false);
@@ -376,7 +374,7 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
snprintf(ssh_user, ssh_user_len, "%s", r.host); snprintf(ssh_user, ssh_user_len, "%s", r.host);
char* remote_command = char* remote_command =
ssh_build_remote_command(server_path, old_args, remote_options, remote_option_count); ssh_build_remote_command(server_path, remote_options, remote_option_count);
if (!remote_command) if (!remote_command)
ssh_child_setup_failed(exec_pipe[1]); ssh_child_setup_failed(exec_pipe[1]);
char** ssh_argv = ssh_build_client_argv(rsh_command, port, ssh_user, remote_command); char** ssh_argv = ssh_build_client_argv(rsh_command, port, ssh_user, remote_command);
+8 -8
View File
@@ -4,17 +4,17 @@
#include "transport_tcp.h" #include "transport_tcp.h"
Client* client_connect_ssh(const char* destination, int port, const char* server_path, Client* client_connect_ssh(const char* destination, int port, const char* server_path,
bool old_args, const char* rsh_command, bool blocking_io, const char* rsh_command, bool blocking_io, char* const* remote_options,
char* const* remote_options, int remote_option_count); int remote_option_count);
/* Build the escaped remote-shell command string (the server program path always /* Build the escaped remote-shell command string (the server program path always
* quoted as one remote-shell word, followed by ` --stdio` and each * quoted as one remote-shell word, followed by ` --stdio` and each
* --remote-option value appended as an individually single-quoted shell word). * --remote-option value appended as an individually single-quoted shell word).
* `old_args` is accepted for CLI/ABI compatibility but no longer disables * The path is always escaped so a metacharacter-bearing --rsync-path can never
* quoting: the path is always escaped so a metacharacter-bearing * be interpreted by the remote shell (the --old-args no-op does not disable
* --rsync-path can never be interpreted by the remote shell. Every * quoting). Every --remote-option value is individually escaped with the '\''
* --remote-option value is individually escaped with the '\'' sequence and * sequence and values with empty/control characters are rejected at the CLI
* values with empty/control characters are rejected at the CLI parse layer. */ * parse layer. */
char* ssh_build_remote_command(const char* server_path, bool old_args, char* const* remote_options, char* ssh_build_remote_command(const char* server_path, char* const* remote_options,
int remote_option_count); int remote_option_count);
/* Build the NULL-terminated child argv for the remote-shell client (argv[0] is /* Build the NULL-terminated child argv for the remote-shell client (argv[0] is
* the exec/execvp program). rsh_command is whitespace-split into leading argv * the exec/execvp program). rsh_command is whitespace-split into leading argv
+16 -31
View File
@@ -5,13 +5,13 @@
static void test_ssh_connect_invalid_dest_no_colon() { static void test_ssh_connect_invalid_dest_no_colon() {
/* cppcheck-suppress constVariablePointer */ /* cppcheck-suppress constVariablePointer */
Client* client = Client* client =
client_connect_ssh("invalid-destination-no-colon", 22, NULL, false, NULL, false, NULL, 0); client_connect_ssh("invalid-destination-no-colon", 22, NULL, NULL, false, NULL, 0);
EXPECT_NULL(client); EXPECT_NULL(client);
} }
static void test_ssh_connect_invalid_dest_empty() { static void test_ssh_connect_invalid_dest_empty() {
/* cppcheck-suppress constVariablePointer */ /* cppcheck-suppress constVariablePointer */
Client* client = client_connect_ssh("", 22, NULL, false, NULL, false, NULL, 0); Client* client = client_connect_ssh("", 22, NULL, NULL, false, NULL, 0);
EXPECT_NULL(client); EXPECT_NULL(client);
} }
@@ -22,7 +22,7 @@ static void test_ssh_connect_malformed() {
setenv("PATH", "", 1); setenv("PATH", "", 1);
/* cppcheck-suppress constVariablePointer */ /* cppcheck-suppress constVariablePointer */
Client* client = client_connect_ssh(":", 22, NULL, false, NULL, false, NULL, 0); Client* client = client_connect_ssh(":", 22, NULL, NULL, false, NULL, 0);
if (saved_path) { if (saved_path) {
setenv("PATH", saved_path, 1); setenv("PATH", saved_path, 1);
@@ -38,7 +38,7 @@ static void test_ssh_connect_malformed() {
* The function launches ssh which will fail to connect, returns a Client. */ * The function launches ssh which will fail to connect, returns a Client. */
static void test_ssh_connect_unreachable() { static void test_ssh_connect_unreachable() {
Client* client = Client* client =
client_connect_ssh("nonexistent.invalid:/remote/path", 22, NULL, false, NULL, false, NULL, 0); client_connect_ssh("nonexistent.invalid:/remote/path", 22, NULL, NULL, false, NULL, 0);
if (client != NULL) { if (client != NULL) {
client_disconnect(client); client_disconnect(client);
client_delete(client); client_delete(client);
@@ -47,23 +47,13 @@ static void test_ssh_connect_unreachable() {
} }
static void test_ssh_remote_command_argument_modes() { static void test_ssh_remote_command_argument_modes() {
char* command = ssh_build_remote_command("fast sync; touch /tmp/pwned", false, NULL, 0); char* command = ssh_build_remote_command("fast sync; touch /tmp/pwned", NULL, 0);
EXPECT_EQ_STR(command, "'fast sync; touch /tmp/pwned' --stdio"); EXPECT_EQ_STR(command, "'fast sync; touch /tmp/pwned' --stdio");
free(command); free(command);
command = ssh_build_remote_command("fast'sync", false, NULL, 0); command = ssh_build_remote_command("fast'sync", NULL, 0);
EXPECT_EQ_STR(command, "'fast'\\''sync' --stdio"); EXPECT_EQ_STR(command, "'fast'\\''sync' --stdio");
free(command); free(command);
/* --old-args no longer disables injection-safe quoting: the path is still one
single-quoted word, even when it carries shell metacharacters. */
command = ssh_build_remote_command("fast sync; touch /tmp/pwned", true, NULL, 0);
EXPECT_EQ_STR(command, "'fast sync; touch /tmp/pwned' --stdio");
free(command);
command = ssh_build_remote_command("fast'sync; rm -rf /", true, NULL, 0);
EXPECT_EQ_STR(command, "'fast'\\''sync; rm -rf /' --stdio");
free(command);
} }
/* The build for a single-word argv is [prog, six -o args, "--", user, command]. */ /* The build for a single-word argv is [prog, six -o args, "--", user, command]. */
@@ -120,12 +110,12 @@ static void test_ssh_build_client_argv_whitespace_command_and_port() {
* returned and no command can run. */ * returned and no command can run. */
static void test_ssh_connect_rejects_option_host() { static void test_ssh_connect_rejects_option_host() {
/* cppcheck-suppress constVariablePointer */ /* cppcheck-suppress constVariablePointer */
Client* client = client_connect_ssh("-oProxyCommand=touch /tmp/pwned:/remote", 22, NULL, false, Client* client =
NULL, false, NULL, 0); client_connect_ssh("-oProxyCommand=touch /tmp/pwned:/remote", 22, NULL, NULL, false, NULL, 0);
EXPECT_NULL(client); EXPECT_NULL(client);
client = client_connect_ssh("-evil:/remote", 22, NULL, false, NULL, false, NULL, 0); client = client_connect_ssh("-evil:/remote", 22, NULL, NULL, false, NULL, 0);
EXPECT_NULL(client); EXPECT_NULL(client);
client = client_connect_ssh("user@:/remote", 22, NULL, false, NULL, false, NULL, 0); client = client_connect_ssh("user@:/remote", 22, NULL, NULL, false, NULL, 0);
EXPECT_NULL(client); EXPECT_NULL(client);
} }
@@ -135,13 +125,13 @@ static void test_ssh_connect_rejects_option_host() {
* ssh_build_remote_command safety boundary for the server path. */ * ssh_build_remote_command safety boundary for the server path. */
static void test_ssh_remote_command_with_remote_options() { static void test_ssh_remote_command_with_remote_options() {
char* noop[] = {"--allow-delete"}; char* noop[] = {"--allow-delete"};
char* command = ssh_build_remote_command("fastsync-server", false, noop, 1); char* command = ssh_build_remote_command("fastsync-server", noop, 1);
EXPECT_EQ_STR(command, "'fastsync-server' --stdio '--allow-delete'"); EXPECT_EQ_STR(command, "'fastsync-server' --stdio '--allow-delete'");
free(command); free(command);
/* Multiple options append in order, each as its own quoted word. */ /* Multiple options append in order, each as its own quoted word. */
char* multi[] = {"-v", "--allow-delete"}; char* multi[] = {"-v", "--allow-delete"};
command = ssh_build_remote_command("srv", false, multi, 2); command = ssh_build_remote_command("srv", multi, 2);
EXPECT_EQ_STR(command, "'srv' --stdio '-v' '--allow-delete'"); EXPECT_EQ_STR(command, "'srv' --stdio '-v' '--allow-delete'");
free(command); free(command);
@@ -150,29 +140,24 @@ static void test_ssh_remote_command_with_remote_options() {
break out into an arbitrary remote command. */ break out into an arbitrary remote command. */
char* val = strdup("--x=un'der; touch /tmp/pwned"); char* val = strdup("--x=un'der; touch /tmp/pwned");
char* dangerous[1] = {val}; char* dangerous[1] = {val};
command = ssh_build_remote_command("srv", false, dangerous, 1); command = ssh_build_remote_command("srv", dangerous, 1);
EXPECT_EQ_STR(command, "'srv' --stdio '--x=un'\\''der; touch /tmp/pwned'"); EXPECT_EQ_STR(command, "'srv' --stdio '--x=un'\\''der; touch /tmp/pwned'");
free(command); free(command);
free(val); free(val);
/* --old-args still quotes both the server path and the remote options. */
command = ssh_build_remote_command("srv", true, multi, 2);
EXPECT_EQ_STR(command, "'srv' --stdio '-v' '--allow-delete'");
free(command);
} }
/* The remote command builder refuses to forward an empty or control-character /* The remote command builder refuses to forward an empty or control-character
* remote option (defense-in-depth independent of the CLI validation). */ * remote option (defense-in-depth independent of the CLI validation). */
static void test_ssh_remote_command_rejects_bad_options() { static void test_ssh_remote_command_rejects_bad_options() {
char* empty[] = {""}; char* empty[] = {""};
EXPECT_NULL(ssh_build_remote_command("srv", false, empty, 1)); EXPECT_NULL(ssh_build_remote_command("srv", empty, 1));
char nl = '\n'; char nl = '\n';
char* newline[] = {&nl}; char* newline[] = {&nl};
EXPECT_NULL(ssh_build_remote_command("srv", false, newline, 1)); EXPECT_NULL(ssh_build_remote_command("srv", newline, 1));
char* with_null[] = {NULL}; char* with_null[] = {NULL};
EXPECT_NULL(ssh_build_remote_command("srv", false, with_null, 1)); EXPECT_NULL(ssh_build_remote_command("srv", with_null, 1));
} }
void test_transport_ssh() { void test_transport_ssh() {