feat(p7-super): implement --super/--no-super safe-subset privilege gate (protocol 2.18.0)

Add the receiver-side --super / --no-super tri-state (Config->super_mode)
under the safe-subset + clear-refusal privilege model: FastSync never
elevates privileges, it only permits super-user attempts that are already
confined fd-relative below the authorized receive root.

- identity: privilege_super_permitted() gate (OFF=false, ON=true, AUTO follows
  geteuid()==0); identity_apply_ownership/_link become no-ops when not
  permitted; --super with no explicit identity policy implies raw numeric-id
  preservation (explicit usermap/groupmap/chown/numeric-ids still win); warn
  exactly once when --super is requested by a non-root receiver.
- file_receive: gate char/block device-node creation on the gate; FIFO/socket
  handling is unchanged.
- wire: trailing super_mode int after the --iconv spec, validated 0..2 in
  receive_privilege_options and validate_received_config; PROTOCOL_VERSION
  2.17.0 -> 2.18.0; version-sensitive tests and docs updated.
- CLI: --super/--no-super parsed explicitly before the generic --no-* branch
  (malformed --super=x rejected); usage text added.
- tests: config wire round-trip + invalid-value rejection, privilege-gate mode
  unit test, CLI parse test, integration transfer + root-gated ownership
  suppression/appliance tests.
- docs: RSYNC_COMPAT --super row + Wave E note, protocol mentions, README.
This commit is contained in:
2026-09-12 12:01:19 +02:00
parent f64d252faf
commit a785ec13c4
13 changed files with 366 additions and 26 deletions
+42 -3
View File
@@ -223,7 +223,7 @@ static void test_parse_args_protocol_accept_current() {
Config* cfg = valid_client_config();
EXPECT_NOT_NULL(cfg);
char* argv_equals[] = {"fastsync", "--source-dir", "/src",
"--dest-dir", "/dst", "--protocol=2.17.0"};
"--dest-dir", "/dst", "--protocol=2.18.0"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 6, argv_equals, positional_args, &positional_count), 0);
@@ -233,7 +233,7 @@ static void test_parse_args_protocol_accept_current() {
cfg = valid_client_config();
EXPECT_NOT_NULL(cfg);
char* argv_space[] = {"fastsync", "--source-dir", "/src", "--dest-dir",
"/dst", "--protocol", "2.17.0"};
"/dst", "--protocol", "2.18.0"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 7, argv_space, positional_args, &positional_count), 0);
EXPECT_EQ_STR(cfg->version, PROTOCOL_VERSION);
@@ -243,7 +243,8 @@ static void test_parse_args_protocol_accept_current() {
/* Any --protocol value other than the current PROTOCOL_VERSION must end in
* failure (parse_args simply stores it; validate_config rejects it up front). */
static void test_parse_args_protocol_rejects_other_versions() {
static const char* const bad_versions[] = {"2.16", "2.15.0", "2.16.0", "216", "31", "abc", ""};
static const char* const bad_versions[] = {"2.17", "2.16", "2.15.0", "2.16.0", "2.17.0",
"216", "31", "abc", ""};
for (size_t i = 0; i < sizeof(bad_versions) / sizeof(bad_versions[0]); i++) {
Config* cfg = valid_client_config();
EXPECT_NOT_NULL(cfg);
@@ -331,6 +332,43 @@ static void test_parse_args_fake_super() {
config_delete(cfg);
}
/* P7 Wave E: --super / --no-super set the receiver-side privilege tri-state
* (they take no argument). The default is AUTO, the last of either flag wins,
* and a malformed inline value ("--super=x") is rejected rather than silently
* treated as --super. */
static void test_parse_args_super() {
Config* cfg = config_create();
EXPECT_EQ_INT(cfg->super_mode, SUPER_MODE_AUTO);
char* argv_on[] = {"fastsync", "--super", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv_on, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->super_mode, SUPER_MODE_ON);
config_delete(cfg);
cfg = config_create();
positional_count = 0;
char* argv_off[] = {"fastsync", "--no-super", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 4, argv_off, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->super_mode, SUPER_MODE_OFF);
config_delete(cfg);
/* Tri-state, not a boolean pair: the last flag wins. */
cfg = config_create();
positional_count = 0;
char* argv_both[] = {"fastsync", "--super", "--no-super", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 5, argv_both, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->super_mode, SUPER_MODE_OFF);
config_delete(cfg);
/* A malformed inline value is a hard unknown-option error. */
cfg = config_create();
positional_count = 0;
char* argv_bad[] = {"fastsync", "--super=x", "/src", "/dst"};
EXPECT_EQ_INT(parse_args(cfg, 4, argv_bad, positional_args, &positional_count), -1);
config_delete(cfg);
}
/* Test parse_args with valid SSH port (long form; -p is now rsync --perms) */
static void test_parse_args_valid_port() {
Config* cfg = config_create();
@@ -3081,6 +3119,7 @@ void test_client_cli() {
test_parse_args_missing_argument_diagnostic();
test_parse_args_xattrs_acls();
test_parse_args_fake_super();
test_parse_args_super();
test_parse_args_partial_progress();
test_parse_args_itemize_changes();
test_parse_args_list_only();