fix(p7-privilege): harden copy-as/super gates, own dirs/specials

- fake-super owner replay honors --no-super and an active --copy-as
- copy-as/identity ownership now applied to directories and special nodes
- reject copy_as_set && !use_metadata (receiver + client --no-preserve)
- daemon refuses --copy-as; add server-side --no-super operator veto
- implement identity_copy_as_refused/identity_copy_as_active
- reject copy-as ids that overflow int32; escape spec in log errors
- copy-as chown EPERM/EACCES logged at ERROR (still non-fatal)
- identity_wire_valid copy-as bounds; CLI help and RSYNC_COMPAT docs
- add unit tests and root-gated integration coverage
This commit is contained in:
2026-09-12 12:34:43 +02:00
parent e6a65d0980
commit fdc0f238c9
18 changed files with 500 additions and 34 deletions
+63
View File
@@ -1741,6 +1741,10 @@ static void test_config_copy_as_wire_roundtrip() {
send_cfg->copy_as_set = cases[i].set;
send_cfg->copy_as_uid = cases[i].uid;
send_cfg->copy_as_gid = cases[i].gid;
/* --copy-as requires the metadata path (the receiver chowns from the
transmitted source ids); a raw frame with copy_as_set but no metadata
is now rejected by validate_received_config. */
send_cfg->use_metadata = cases[i].set;
bool sent = config_send(p[1], send_cfg);
int status;
waitpid(pid, &status, 0);
@@ -1814,6 +1818,63 @@ static void test_config_receive_rejects_negative_copy_as() {
}
}
/* --copy-as forces ownership through the metadata path. A frame that sets
copy_as_set but not use_metadata would pass the receiver's privilege gate
while chowning nothing, so validate_received_config must reject it (and the
sender observes the rejection as a failed config_send). */
static void test_config_receive_rejects_copy_as_without_metadata() {
if (is_running_under_valgrind())
return;
Config* c = config_create();
EXPECT_NOT_NULL(c);
c->send_directory = str_dup("/src");
c->receive_root_directory = str_dup("/dst");
c->copy_as_set = true;
c->copy_as_uid = 1000;
c->copy_as_gid = 1000;
c->use_metadata = false;
EXPECT_FALSE(roundtrip_config_ok(c));
config_delete(c);
/* With metadata enabled the same block is accepted. */
c = config_create();
EXPECT_NOT_NULL(c);
c->send_directory = str_dup("/src");
c->receive_root_directory = str_dup("/dst");
c->copy_as_set = true;
c->copy_as_uid = 1000;
c->copy_as_gid = 1000;
c->use_metadata = true;
EXPECT_TRUE(roundtrip_config_ok(c));
config_delete(c);
}
/* 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
mode is OFF (an operator veto), and never when --copy-as is unset. */
static void test_identity_copy_as_refused() {
Config* c = config_create();
EXPECT_NOT_NULL(c);
EXPECT_FALSE(identity_copy_as_refused(c));
EXPECT_FALSE(identity_copy_as_refused(NULL));
c->copy_as_set = true;
c->super_mode = SUPER_MODE_AUTO;
if (geteuid() == 0) {
EXPECT_FALSE(identity_copy_as_refused(c)); /* AUTO permits as root */
c->super_mode = SUPER_MODE_ON;
EXPECT_FALSE(identity_copy_as_refused(c));
c->super_mode = SUPER_MODE_OFF;
EXPECT_TRUE(identity_copy_as_refused(c));
} else {
/* Unprivileged: refused regardless of the mode. */
EXPECT_TRUE(identity_copy_as_refused(c));
c->super_mode = SUPER_MODE_OFF;
EXPECT_TRUE(identity_copy_as_refused(c));
}
config_delete(c);
}
/* P7 Wave E: privilege_super_permitted() maps the super_mode tri-state. OFF
forbids super-user activities even for root; ON and AUTO permit the confined
attempt (matching FastSync's historical best-effort behavior, where the kernel
@@ -1888,8 +1949,10 @@ void test_config() {
test_config_receive_rejects_invalid_super_mode();
test_config_copy_as_wire_roundtrip();
test_config_receive_rejects_negative_copy_as();
test_config_receive_rejects_copy_as_without_metadata();
test_config_receive_with_validate_rejects();
}
test_identity_copy_as_refused();
test_privilege_super_permitted_modes();
test_config_delete_timing_early_helper();
test_config_is_remote_dest();