From 393a440c78c1beed3c20b1dae69e9c40929551b0 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 16:27:45 +0200 Subject: [PATCH] feat: add rsync old-args compatibility mode --- RSYNC_COMPAT.md | 2 +- src/client/client_cli.c | 1 + src/client/client_send.c | 2 +- src/client/usage.c | 2 ++ src/shared/config.c | 1 + src/shared/config.h | 1 + src/shared/transport_ssh.c | 54 +++++++++++++++++++++++++++++++++++--- src/shared/transport_ssh.h | 4 ++- tests/test_client_cli.c | 12 +++++++++ tests/test_transport_ssh.c | 23 +++++++++++++--- 10 files changed, 92 insertions(+), 10 deletions(-) diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index 4b236ec..24d3b43 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -211,7 +211,7 @@ This document maps rsync's full feature set to FastSync's current implementation | Max data/string/chunk sizes | Prevent OOM attacks | ✅ Implemented | Per-message limits | | Per-connection memory limit | 1GB per connection | ✅ Implemented | `MAX_CONNECTION_MEMORY` | | `--trust-sender` | Trust remote sender's file list | ❌ Not Implemented | | -| `--old-args` | Disable modern arg protection | ❌ Not Implemented | | +| `--old-args` | Disable modern arg protection | ✅ Implemented | SSH-only legacy mode; restores raw remote command construction and permits shell interpretation of the configured server path | | `--ignore-missing-args` | Ignore missing source args | ❌ Not Implemented | | | `--delete-missing-args` | Delete missing source args | ❌ Not Implemented | | diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 191cc07..815b3f3 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -146,6 +146,7 @@ static const OptionEntry OPTION_TABLE[] = { {"--backup", NULL, OPT_FLAG, offsetof(Config, backup)}, {"--stats", NULL, OPT_FLAG, offsetof(Config, stats)}, {"--partial", NULL, OPT_FLAG, offsetof(Config, partial)}, + {"--old-args", NULL, OPT_FLAG, offsetof(Config, old_args)}, {"--links", "-l", OPT_FLAG, offsetof(Config, follow_symlinks)}, {"--copy-links", NULL, OPT_FLAG, offsetof(Config, copy_links)}, {"--safe-links", NULL, OPT_FLAG, offsetof(Config, safe_links)}, diff --git a/src/client/client_send.c b/src/client/client_send.c index 0b4ca5c..4f103a4 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -46,7 +46,7 @@ static Client* connect_transfer_client(const Config* config) { return NULL; } return client_connect_ssh(config->ssh_destination, config->ssh_port, - config->fastsync_server_path); + config->fastsync_server_path, config->old_args); } Client* client = client_create(); diff --git a/src/client/usage.c b/src/client/usage.c index 2f800af..f6f0d26 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -62,6 +62,8 @@ void print_usage(void) { printf(" --partial-dir Directory for partial files\n"); printf(" --fastsync-server-path \n"); printf(" Path to fastsync-server on remote (default: fastsync-server)\n"); + printf( + " --old-args Disable safe SSH command argument quoting (legacy compatibility)\n"); printf(" -l, --links Copy symlinks as symlinks\n"); printf(" --copy-links Transform symlinks into referent files\n"); printf(" --safe-links Skip symlinks that point outside transfer tree\n"); diff --git a/src/shared/config.c b/src/shared/config.c index efc2708..f250869 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -82,6 +82,7 @@ static void config_set_defaults(Config* config) { config->relative = false; config->rsh_command = NULL; config->rsync_path = NULL; + config->old_args = false; config->temp_dir = NULL; config->compare_dest = NULL; config->copy_dest = NULL; diff --git a/src/shared/config.h b/src/shared/config.h index 4a218b0..ed596b8 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -96,6 +96,7 @@ typedef struct Config { // Issue #130: Remote shell/connection options char* rsh_command; char* rsync_path; + bool old_args; char* temp_dir; char* compare_dest; char* copy_dest; diff --git a/src/shared/transport_ssh.c b/src/shared/transport_ssh.c index 4941526..8261191 100644 --- a/src/shared/transport_ssh.c +++ b/src/shared/transport_ssh.c @@ -3,6 +3,7 @@ #include "utils.h" #include #include +#include #include #include #include @@ -68,7 +69,52 @@ static int parse_remote_dest(const char* dest, RemoteDest* r) { return 0; } -Client* client_connect_ssh(const char* destination, int port, const char* server_path) { +char* ssh_build_remote_command(const char* server_path, bool old_args) { + const char* path = server_path ? server_path : "fastsync-server"; + const char* suffix = " --stdio"; + size_t path_len = strlen(path); + size_t suffix_len = strlen(suffix); + + if (old_args) { + if (path_len > SIZE_MAX - suffix_len - 1) + return NULL; + char* command = malloc(path_len + suffix_len + 1); + if (!command) + return NULL; + memcpy(command, path, path_len); + memcpy(command + path_len, suffix, suffix_len + 1); + return command; + } + + /* Quote the executable as one remote-shell word. This is the default safety boundary. */ + size_t quote_count = 0; + for (const char* p = path; *p; p++) + if (*p == '\'') + quote_count++; + if (path_len > SIZE_MAX - suffix_len - 4 || + quote_count > (SIZE_MAX - path_len - suffix_len - 4) / 4) + return NULL; + size_t command_len = path_len + quote_count * 4 + suffix_len + 4; + char* command = malloc(command_len + 1); + if (!command) + return NULL; + char* out = command; + *out++ = '\''; + for (const char* p = path; *p; p++) { + if (*p == '\'') { + memcpy(out, "'\\''", 4); + out += 4; + } else { + *out++ = *p; + } + } + *out++ = '\''; + memcpy(out, suffix, suffix_len + 1); + return command; +} + +Client* client_connect_ssh(const char* destination, int port, const char* server_path, + bool old_args) { RemoteDest r; if (parse_remote_dest(destination, &r) != 0) { fprintf(stderr, "Invalid remote destination: %s\n", destination); @@ -135,6 +181,9 @@ Client* client_connect_ssh(const char* destination, int port, const char* server char* ssh_argv[16]; int ac = 0; char port_str[16]; + char* remote_command = ssh_build_remote_command(server_path, old_args); + if (!remote_command) + _exit(1); ssh_argv[ac++] = "ssh"; ssh_argv[ac++] = "-o"; ssh_argv[ac++] = "Compression=no"; @@ -148,8 +197,7 @@ Client* client_connect_ssh(const char* destination, int port, const char* server ssh_argv[ac++] = port_str; } ssh_argv[ac++] = ssh_user; - ssh_argv[ac++] = (char*)(server_path ? server_path : "fastsync-server"); - ssh_argv[ac++] = "--stdio"; + ssh_argv[ac++] = remote_command; ssh_argv[ac] = NULL; execvp("ssh", ssh_argv); log_perror("exec of ssh failed"); diff --git a/src/shared/transport_ssh.h b/src/shared/transport_ssh.h index 315f37d..e46c687 100644 --- a/src/shared/transport_ssh.h +++ b/src/shared/transport_ssh.h @@ -3,6 +3,8 @@ #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); +char* ssh_build_remote_command(const char* server_path, bool old_args); #endif diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 656fd40..de0c16e 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -339,6 +339,17 @@ static void test_parse_args_archive() { config_delete(cfg); } +static void test_parse_args_old_args() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--old-args", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); + EXPECT_TRUE(cfg->old_args); + config_delete(cfg); +} + void test_client_cli() { test_validate_config_required_paths(); test_validate_config_incompatible_options(); @@ -360,4 +371,5 @@ void test_client_cli() { test_parse_args_unknown_option(); test_parse_args_rejects_unimplemented_options(); test_parse_args_archive(); + test_parse_args_old_args(); } diff --git a/tests/test_transport_ssh.c b/tests/test_transport_ssh.c index e085cbb..bb83cd9 100644 --- a/tests/test_transport_ssh.c +++ b/tests/test_transport_ssh.c @@ -4,13 +4,13 @@ static void test_ssh_connect_invalid_dest_no_colon() { /* cppcheck-suppress constVariablePointer */ - Client* client = client_connect_ssh("invalid-destination-no-colon", 22, NULL); + Client* client = client_connect_ssh("invalid-destination-no-colon", 22, NULL, false); EXPECT_NULL(client); } static void test_ssh_connect_invalid_dest_empty() { /* cppcheck-suppress constVariablePointer */ - Client* client = client_connect_ssh("", 22, NULL); + Client* client = client_connect_ssh("", 22, NULL, false); EXPECT_NULL(client); } @@ -18,7 +18,7 @@ static void test_ssh_connect_invalid_dest_empty() { * parse_remote_dest succeeds, ssh is exec'd and fails, but the function * creates a Client that must be cleaned up. */ static void test_ssh_connect_malformed() { - Client* client = client_connect_ssh(":", 22, NULL); + Client* client = client_connect_ssh(":", 22, NULL, false); /* ssh binary exists, so exec succeeds; the function returns a Client. * We just verify it doesn't crash and clean up properly. */ if (client != NULL) { @@ -31,7 +31,7 @@ static void test_ssh_connect_malformed() { /* Test client_connect_ssh with valid format but unreachable host. * The function launches ssh which will fail to connect, returns a Client. */ static void test_ssh_connect_unreachable() { - Client* client = client_connect_ssh("nonexistent.invalid:/remote/path", 22, NULL); + Client* client = client_connect_ssh("nonexistent.invalid:/remote/path", 22, NULL, false); if (client != NULL) { client_disconnect(client); client_delete(client); @@ -39,9 +39,24 @@ static void test_ssh_connect_unreachable() { EXPECT_TRUE(true); } +static void test_ssh_remote_command_argument_modes() { + char* command = ssh_build_remote_command("fast sync; touch /tmp/pwned", false); + EXPECT_EQ_STR(command, "'fast sync; touch /tmp/pwned' --stdio"); + free(command); + + command = ssh_build_remote_command("fast'sync", false); + EXPECT_EQ_STR(command, "'fast'\\''sync' --stdio"); + free(command); + + command = ssh_build_remote_command("fast sync; touch /tmp/pwned", true); + EXPECT_EQ_STR(command, "fast sync; touch /tmp/pwned --stdio"); + free(command); +} + void test_transport_ssh() { test_ssh_connect_invalid_dest_no_colon(); test_ssh_connect_invalid_dest_empty(); test_ssh_connect_malformed(); test_ssh_connect_unreachable(); + test_ssh_remote_command_argument_modes(); }