fix(identity): rsync ownership parity for numeric-ids, dirs, maps, fake-super (#286, #294)

- #286: --numeric-ids is a mapping modifier only; it no longer activates
  chown by itself (identity_active_enabled/owner/group predicates), and
  --fake-super stores the resolved mapping instead of real-chowning.
- #286: apply owner/group to directories via the deferred directory
  metadata path; capture+transmit+apply directory xattrs/ACLs (-aX/-aA),
  including default ACLs, in STATUS_MKDIR/STATUS_DIR_TIMES.
- #294: --usermap/--groupmap support inclusive ranges, '*', empty FROM
  (unnamed ids), and receiver-side TO name resolution; --chown mixing with
  a same-side map is rejected like rsync.
- Protocol 2.22.0 -> 2.23.0 (map wire entry gains from_hi + to_name;
  dir frames gain a bounded xattr block).
This commit is contained in:
2026-09-15 22:10:02 +02:00
parent 23552e823d
commit 6144c7fc7f
26 changed files with 1115 additions and 376 deletions
+39 -15
View File
@@ -1345,13 +1345,19 @@ static void test_config_identity_wire_roundtrip() {
send_cfg->usermap_count = 2;
send_cfg->usermap = calloc(2, sizeof(IdentityMap));
send_cfg->usermap[0].from = IDENTITY_MATCH_ANY;
send_cfg->usermap[0].from_hi = IDENTITY_MATCH_ANY;
send_cfg->usermap[0].to = 65534;
send_cfg->usermap[0].to_name = NULL;
send_cfg->usermap[1].from = 1000;
send_cfg->usermap[1].from_hi = 1000;
send_cfg->usermap[1].to = 1000;
send_cfg->usermap[1].to_name = NULL;
send_cfg->groupmap_count = 1;
send_cfg->groupmap = calloc(1, sizeof(IdentityMap));
send_cfg->groupmap[0].from = 0;
send_cfg->groupmap[0].from_hi = 0;
send_cfg->groupmap[0].to = IDENTITY_CURRENT;
send_cfg->groupmap[0].to_name = str_dup("root");
int p[2];
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
@@ -1367,9 +1373,12 @@ static void test_config_identity_wire_roundtrip() {
ok = recv->numeric_ids && recv->chown_uid_set && recv->chown_uid == 1001 &&
recv->chown_gid_set && recv->chown_gid == IDENTITY_CURRENT && recv->usermap_count == 2 &&
recv->groupmap_count == 1 && recv->usermap[0].from == IDENTITY_MATCH_ANY &&
recv->usermap[0].to == 65534 && recv->usermap[1].from == 1000 &&
recv->usermap[1].to == 1000 && recv->groupmap[0].from == 0 &&
recv->groupmap[0].to == IDENTITY_CURRENT;
recv->usermap[0].from_hi == IDENTITY_MATCH_ANY && recv->usermap[0].to == 65534 &&
recv->usermap[0].to_name == NULL && recv->usermap[1].from == 1000 &&
recv->usermap[1].from_hi == 1000 && recv->usermap[1].to == 1000 &&
recv->groupmap[0].from == 0 && recv->groupmap[0].from_hi == 0 &&
recv->groupmap[0].to == IDENTITY_CURRENT && recv->groupmap[0].to_name != NULL &&
strcmp(recv->groupmap[0].to_name, "root") == 0;
}
config_delete(recv);
close(p[0]);
@@ -1399,7 +1408,8 @@ static void test_config_receive_rejects_invalid_identity() {
c->receive_root_directory = str_dup("/dst");
c->usermap_count = 1;
c->usermap = calloc(1, sizeof(IdentityMap));
c->usermap[0].from = -2; /* below IDENTITY_MATCH_ANY */
c->usermap[0].from = -3; /* below IDENTITY_MATCH_UNNAMED */
c->usermap[0].from_hi = -3;
c->usermap[0].to = 0;
EXPECT_FALSE(roundtrip_config_ok(c));
config_delete(c);
@@ -2101,8 +2111,10 @@ static void test_identity_explicit_ownership_requested() {
}
/* P7 Wave E hardening (A3): --super no longer implies raw numeric-id
preservation, so it must never enable ownership application on its own; an
explicit identity flag is required. */
preservation, so it must never enable ownership application on its own.
#286: --numeric-ids is a mapping MODIFIER only and is likewise inert on its
own; a real ownership request (-o/-g or an explicit identity flag) is
required to activate chown. */
static void test_super_does_not_imply_numeric() {
Config* c = config_create();
EXPECT_NOT_NULL(c);
@@ -2112,6 +2124,9 @@ static void test_super_does_not_imply_numeric() {
EXPECT_FALSE(identity_active_enabled());
c->numeric_ids = true;
EXPECT_TRUE(identity_set_active(c));
EXPECT_FALSE(identity_active_enabled()); /* mapping modifier only */
c->preserve_owner = true;
EXPECT_TRUE(identity_set_active(c));
EXPECT_TRUE(identity_active_enabled());
identity_clear_active();
config_delete(c);
@@ -2635,13 +2650,19 @@ static void golden_config_populate(Config* c) {
c->usermap_count = 2;
c->usermap = calloc(2, sizeof(IdentityMap));
c->usermap[0].from = IDENTITY_MATCH_ANY;
c->usermap[0].from_hi = IDENTITY_MATCH_ANY;
c->usermap[0].to = 1000;
c->usermap[0].to_name = NULL;
c->usermap[1].from = 5;
c->usermap[1].from_hi = 9;
c->usermap[1].to = 6;
c->usermap[1].to_name = NULL;
c->groupmap_count = 1;
c->groupmap = calloc(1, sizeof(IdentityMap));
c->groupmap[0].from = 7;
c->groupmap[0].from_hi = 7;
c->groupmap[0].to = 8;
c->groupmap[0].to_name = str_dup("root");
c->preserve_atimes = true;
c->preserve_crtimes = false;
c->omit_dir_times = true;
@@ -2665,14 +2686,14 @@ static void golden_config_populate(Config* c) {
c->copy_as_gid = 222;
}
/* The pinned golden frame (protocol 2.22.0). The values below are the only
/* The pinned golden frame (protocol 2.23.0). The values below are the only
* thing that ties the generated table to the historical wire format; update
* them ONLY with a PROTOCOL_VERSION bump and a documented reason. The 2.22.0
* preserve-attribute split appends four serialized bools
* (preserve_perms/times/owner/group) to CONFIG_WIRE_METADATA_TIMES_FIELDS after
* omit_link_times. */
#define GOLDEN_WIRE_LEN 653
#define GOLDEN_WIRE_HASH 95530566005420798ULL
* them ONLY with a PROTOCOL_VERSION bump and a documented reason. The 2.23.0
* ownership-parity wave extends each --usermap/--groupmap wire entry with
* from_hi + a TO-name string (and the earlier preserve-attribute split appended
* four serialized bools after omit_link_times). */
#define GOLDEN_WIRE_LEN 693
#define GOLDEN_WIRE_HASH 6341115972171444885ULL
static unsigned long long fnv1a_64(const unsigned char* buf, size_t len) {
unsigned long long h = 1469598103934665603ULL;
@@ -2754,7 +2775,7 @@ static unsigned long long capture_wire_hash(const Config* cfg, size_t* out_len)
return h;
}
/* Byte-for-byte wire compatibility guard (protocol 2.22.0). The expected hash
/* Byte-for-byte wire compatibility guard (protocol 2.23.0). The expected hash
* pins the pre-X-macro byte stream; the refactor MUST NOT change it. */
static void test_config_wire_golden() {
if (is_running_under_valgrind())
@@ -2815,7 +2836,10 @@ static void test_config_wire_golden_receive() {
ok = ok && recv->super_mode == SUPER_MODE_ON;
ok = ok && recv->chown_uid == 1234 && recv->chown_gid == 5678;
ok = ok && recv->usermap_count == 2 && recv->usermap[0].from == IDENTITY_MATCH_ANY &&
recv->usermap[0].to == 1000 && recv->usermap[1].from == 5 && recv->usermap[1].to == 6;
recv->usermap[0].from_hi == IDENTITY_MATCH_ANY && recv->usermap[0].to == 1000 &&
recv->usermap[1].from == 5 && recv->usermap[1].from_hi == 9 && recv->usermap[1].to == 6;
ok = ok && recv->groupmap_count == 1 && recv->groupmap[0].from == 7 &&
recv->groupmap[0].to_name != NULL && strcmp(recv->groupmap[0].to_name, "root") == 0;
ok = ok && recv->basis_count == 2 && recv->basis_dirs[0].type == BASIS_DEST_COMPARE &&
recv->basis_dirs[1].type == BASIS_DEST_LINK;
ok = ok && recv->module != NULL && strcmp(recv->module, "goldenmod") == 0;