- #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:
+343
-97
@@ -43,14 +43,27 @@ typedef struct {
|
||||
* so they are tracked separately from the explicit ownership gate. */
|
||||
bool preserve_owner;
|
||||
bool preserve_group;
|
||||
/* --fake-super: when active the receiver must only RECORD the (resolved)
|
||||
* ownership in the reserved xattr, never perform a real chown. Snapshotted
|
||||
* so the fd-relative ownership helpers can suppress the chown without a
|
||||
* Config argument. */
|
||||
bool fake_super;
|
||||
bool set;
|
||||
} IdentityActive;
|
||||
|
||||
static IdentityActive g_identity;
|
||||
|
||||
static void identity_active_reset(void) {
|
||||
free(g_identity.usermap);
|
||||
free(g_identity.groupmap);
|
||||
if (g_identity.usermap) {
|
||||
for (int i = 0; i < g_identity.usermap_count; i++)
|
||||
free(g_identity.usermap[i].to_name);
|
||||
free(g_identity.usermap);
|
||||
}
|
||||
if (g_identity.groupmap) {
|
||||
for (int i = 0; i < g_identity.groupmap_count; i++)
|
||||
free(g_identity.groupmap[i].to_name);
|
||||
free(g_identity.groupmap);
|
||||
}
|
||||
g_identity.usermap = NULL;
|
||||
g_identity.groupmap = NULL;
|
||||
g_identity.usermap_count = 0;
|
||||
@@ -66,6 +79,7 @@ static void identity_active_reset(void) {
|
||||
g_identity.copy_as_gid = 0;
|
||||
g_identity.preserve_owner = false;
|
||||
g_identity.preserve_group = false;
|
||||
g_identity.fake_super = false;
|
||||
g_identity.set = false;
|
||||
}
|
||||
|
||||
@@ -88,20 +102,35 @@ bool identity_set_active(const Config* config) {
|
||||
g_identity.copy_as_gid = config->copy_as_gid;
|
||||
g_identity.preserve_owner = config->preserve_owner;
|
||||
g_identity.preserve_group = config->preserve_group;
|
||||
g_identity.fake_super = config->fake_super;
|
||||
if (config->usermap_count > 0) {
|
||||
g_identity.usermap = calloc((size_t)config->usermap_count, sizeof(IdentityMap));
|
||||
if (!g_identity.usermap)
|
||||
goto alloc_failed;
|
||||
memcpy(g_identity.usermap, config->usermap,
|
||||
(size_t)config->usermap_count * sizeof(IdentityMap));
|
||||
for (int i = 0; i < config->usermap_count; i++) {
|
||||
g_identity.usermap[i] = config->usermap[i];
|
||||
g_identity.usermap[i].to_name =
|
||||
config->usermap[i].to_name ? str_dup(config->usermap[i].to_name) : NULL;
|
||||
if (config->usermap[i].to_name && !g_identity.usermap[i].to_name) {
|
||||
g_identity.usermap_count = i; /* free only the entries already duplicated */
|
||||
goto alloc_failed;
|
||||
}
|
||||
}
|
||||
g_identity.usermap_count = config->usermap_count;
|
||||
}
|
||||
if (config->groupmap_count > 0) {
|
||||
g_identity.groupmap = calloc((size_t)config->groupmap_count, sizeof(IdentityMap));
|
||||
if (!g_identity.groupmap)
|
||||
goto alloc_failed;
|
||||
memcpy(g_identity.groupmap, config->groupmap,
|
||||
(size_t)config->groupmap_count * sizeof(IdentityMap));
|
||||
for (int i = 0; i < config->groupmap_count; i++) {
|
||||
g_identity.groupmap[i] = config->groupmap[i];
|
||||
g_identity.groupmap[i].to_name =
|
||||
config->groupmap[i].to_name ? str_dup(config->groupmap[i].to_name) : NULL;
|
||||
if (config->groupmap[i].to_name && !g_identity.groupmap[i].to_name) {
|
||||
g_identity.groupmap_count = i;
|
||||
goto alloc_failed;
|
||||
}
|
||||
}
|
||||
g_identity.groupmap_count = config->groupmap_count;
|
||||
}
|
||||
g_identity.set = true;
|
||||
@@ -157,30 +186,28 @@ bool privilege_super_mode_permitted(SuperMode mode) {
|
||||
}
|
||||
|
||||
bool identity_active_enabled(void) {
|
||||
/* numeric_ids is included: this set only gates identity_apply_ownership,
|
||||
which runs only when metadata is present (a -M/--preserve transfer). A
|
||||
standalone --numeric-ids (no ownership-affecting flag) carries no
|
||||
metadata, never reaches identity_apply_ownership, and therefore correctly
|
||||
stays inert; combined with -M it activates raw-id application. --super /
|
||||
--no-super does NOT enable ownership: it only permits or forbids the
|
||||
already-requested super-user activities, so a --super with no explicit
|
||||
identity flag must never silently apply client-chosen ownership. */
|
||||
/* --numeric-ids is deliberately NOT included: it is a mapping MODIFIER (use
|
||||
* the transmitted numeric id raw instead of a name lookup), not a request to
|
||||
* change ownership. rsync's --numeric-ids on its own never chowns anything;
|
||||
* it only changes how an already-requested -o/-g/map resolves. Ownership is
|
||||
* activated only by an explicit request: --chown/--usermap/--groupmap/
|
||||
* --copy-as or a preserve-source -o/--owner / -g/--group. --super/--no-super
|
||||
* likewise does NOT enable ownership: it only permits or forbids the
|
||||
* already-requested super-user activities. */
|
||||
return g_identity.set &&
|
||||
(g_identity.numeric_ids || g_identity.chown_uid_set || g_identity.chown_gid_set ||
|
||||
g_identity.usermap_count > 0 || g_identity.groupmap_count > 0 || g_identity.copy_as_set ||
|
||||
g_identity.preserve_owner || g_identity.preserve_group);
|
||||
(g_identity.chown_uid_set || g_identity.chown_gid_set || g_identity.usermap_count > 0 ||
|
||||
g_identity.groupmap_count > 0 || g_identity.copy_as_set || g_identity.preserve_owner ||
|
||||
g_identity.preserve_group);
|
||||
}
|
||||
|
||||
bool identity_owner_requested(void) {
|
||||
return g_identity.set &&
|
||||
(g_identity.copy_as_set || g_identity.chown_uid_set || g_identity.numeric_ids ||
|
||||
g_identity.preserve_owner || g_identity.usermap_count > 0);
|
||||
return g_identity.set && (g_identity.copy_as_set || g_identity.chown_uid_set ||
|
||||
g_identity.preserve_owner || g_identity.usermap_count > 0);
|
||||
}
|
||||
|
||||
bool identity_group_requested(void) {
|
||||
return g_identity.set &&
|
||||
(g_identity.copy_as_set || g_identity.chown_gid_set || g_identity.numeric_ids ||
|
||||
g_identity.preserve_group || g_identity.groupmap_count > 0);
|
||||
return g_identity.set && (g_identity.copy_as_set || g_identity.chown_gid_set ||
|
||||
g_identity.preserve_group || g_identity.groupmap_count > 0);
|
||||
}
|
||||
|
||||
bool identity_ownership_requested(const Config* config) {
|
||||
@@ -228,6 +255,29 @@ bool identity_copy_as_refused(const Config* config) {
|
||||
return geteuid() != 0 || config->super_mode == SUPER_MODE_OFF;
|
||||
}
|
||||
|
||||
/* Validate one received FROM:TO map rule. `from` is a single id, the LOW end
|
||||
* of an inclusive range, IDENTITY_MATCH_ANY, or IDENTITY_MATCH_UNNAMED; a
|
||||
* sentinel FROM must carry the same value in from_hi. `to` is a non-negative
|
||||
* id, IDENTITY_CURRENT, or ignored when a bounded receiver-resolved `to_name`
|
||||
* is present. */
|
||||
static bool identity_wire_map_valid(const IdentityMap* map) {
|
||||
if (!map)
|
||||
return false;
|
||||
if (map->from < IDENTITY_MATCH_UNNAMED)
|
||||
return false;
|
||||
if (map->from < 0) {
|
||||
if (map->from_hi != map->from)
|
||||
return false;
|
||||
} else if (map->from_hi < map->from) {
|
||||
return false;
|
||||
}
|
||||
if (map->to < IDENTITY_CURRENT)
|
||||
return false;
|
||||
if (map->to_name && strlen(map->to_name) > 255)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool identity_wire_valid(const Config* config) {
|
||||
if (!config)
|
||||
return false;
|
||||
@@ -239,11 +289,11 @@ bool identity_wire_valid(const Config* config) {
|
||||
if (config->chown_gid_set && config->chown_gid < IDENTITY_MATCH_ANY)
|
||||
return false;
|
||||
for (int i = 0; i < config->usermap_count; i++) {
|
||||
if (config->usermap[i].from < IDENTITY_MATCH_ANY || config->usermap[i].to < IDENTITY_CURRENT)
|
||||
if (!identity_wire_map_valid(&config->usermap[i]))
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < config->groupmap_count; i++) {
|
||||
if (config->groupmap[i].from < IDENTITY_MATCH_ANY || config->groupmap[i].to < IDENTITY_CURRENT)
|
||||
if (!identity_wire_map_valid(&config->groupmap[i]))
|
||||
return false;
|
||||
}
|
||||
/* Defense-in-depth: a --copy-as block must never carry a negative (sentinel)
|
||||
@@ -301,15 +351,137 @@ static int identity_resolve_token(const char* token, bool is_group, int32_t* out
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int identity_append_rule(IdentityMap** map, int* count, int32_t from, int32_t to) {
|
||||
static bool identity_all_digits(const char* token) {
|
||||
if (!token || *token == '\0')
|
||||
return false;
|
||||
for (const char* p = token; *p; p++)
|
||||
if (*p < '0' || *p > '9')
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool identity_token_has_glob(const char* token) {
|
||||
return token && (strchr(token, '*') || strchr(token, '?') || strchr(token, '['));
|
||||
}
|
||||
|
||||
/* Parse a --usermap/--groupmap FROM token into a matcher (from/from_hi). rsync
|
||||
* accepts a name, a numeric id, an inclusive LOW-HIGH range, '*' (any id), or an
|
||||
* empty token (ids with no name on the sender). Returns 0 on success, -1 on a
|
||||
* malformed token or an unresolvable sender-side name. */
|
||||
static int identity_parse_from(const char* token, bool is_group, int32_t* out_from,
|
||||
int32_t* out_hi) {
|
||||
if (token[0] == '\0') {
|
||||
*out_from = IDENTITY_MATCH_UNNAMED;
|
||||
*out_hi = IDENTITY_MATCH_UNNAMED;
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(token, "*") == 0) {
|
||||
*out_from = IDENTITY_MATCH_ANY;
|
||||
*out_hi = IDENTITY_MATCH_ANY;
|
||||
return 0;
|
||||
}
|
||||
const char* num = token[0] == '@' ? token + 1 : token;
|
||||
if (identity_all_digits(num)) {
|
||||
int32_t id;
|
||||
if (identity_resolve_token(token, is_group, &id) != 0)
|
||||
return -1;
|
||||
*out_from = id;
|
||||
*out_hi = id;
|
||||
return 0;
|
||||
}
|
||||
/* An inclusive LOW-HIGH numeric range. */
|
||||
const char* dash = strchr(num, '-');
|
||||
if (dash && dash != num && dash[1] != '\0' && strchr(dash + 1, '-') == NULL) {
|
||||
size_t lo_len = (size_t)(dash - num);
|
||||
size_t hi_len = strlen(dash + 1);
|
||||
char low[16];
|
||||
char high[16];
|
||||
if (lo_len < sizeof(low) && hi_len < sizeof(high)) {
|
||||
memcpy(low, num, lo_len);
|
||||
low[lo_len] = '\0';
|
||||
memcpy(high, dash + 1, hi_len);
|
||||
high[hi_len] = '\0';
|
||||
if (identity_all_digits(low) && identity_all_digits(high)) {
|
||||
char* endptr = NULL;
|
||||
errno = 0;
|
||||
long lo = strtol(low, &endptr, 10);
|
||||
if (errno != 0 || !endptr || *endptr != '\0')
|
||||
return -1;
|
||||
errno = 0;
|
||||
long hi = strtol(high, &endptr, 10);
|
||||
if (errno != 0 || !endptr || *endptr != '\0' || hi < lo || hi > INT32_MAX)
|
||||
return -1;
|
||||
*out_from = (int32_t)lo;
|
||||
*out_hi = (int32_t)hi;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
/* Not a numeric LOW-HIGH range: fall through and treat as a name (a
|
||||
* hyphenated account name like "wayne-smith" must still resolve). */
|
||||
}
|
||||
/* A sender-side name. A wildcard other than the bare '*' is matched by rsync
|
||||
* against the sender's names; because FastSync transmits numeric ids only, the
|
||||
* receiver cannot evaluate it, so reject rather than silently mis-match. */
|
||||
if (identity_token_has_glob(token)) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"%smap FROM '%s': name wildcards other than '*' are not supported "
|
||||
"(FastSync transmits numeric ids, so sender names are unavailable on the "
|
||||
"receiver)",
|
||||
is_group ? "--group" : "--user", token);
|
||||
return -1;
|
||||
}
|
||||
int32_t id;
|
||||
if (identity_resolve_token(token, is_group, &id) != 0)
|
||||
return -1;
|
||||
*out_from = id;
|
||||
*out_hi = id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Parse a --usermap/--groupmap TO token. '*', a bare numeric id, or an @N id is
|
||||
* stored numerically; every other non-empty token is a NAME resolved on the
|
||||
* RECEIVER at apply time (rsync resolves TO names against the receiving side).
|
||||
* Returns 0 on success, -1 on an empty/malformed token. */
|
||||
static int identity_parse_to(const char* token, bool is_group, int32_t* out_to, char** out_name) {
|
||||
if (token[0] == '\0') {
|
||||
log_message(LOG_LEVEL_ERROR, "%smap TO value is missing", is_group ? "--group" : "--user");
|
||||
return -1;
|
||||
}
|
||||
if (strcmp(token, "*") == 0) {
|
||||
*out_to = IDENTITY_CURRENT;
|
||||
*out_name = NULL;
|
||||
return 0;
|
||||
}
|
||||
const char* num = token[0] == '@' ? token + 1 : token;
|
||||
if (identity_all_digits(num)) {
|
||||
int32_t id;
|
||||
if (identity_resolve_token(token, is_group, &id) != 0)
|
||||
return -1;
|
||||
*out_to = id;
|
||||
*out_name = NULL;
|
||||
return 0;
|
||||
}
|
||||
if (identity_token_has_glob(token)) {
|
||||
log_message(LOG_LEVEL_ERROR, "%smap TO '%s' may not contain a wildcard",
|
||||
is_group ? "--group" : "--user", token);
|
||||
return -1;
|
||||
}
|
||||
char* name = str_dup(token);
|
||||
if (!name)
|
||||
return -1;
|
||||
*out_to = 0;
|
||||
*out_name = name;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int identity_append_rule(IdentityMap** map, int* count, const IdentityMap* rule) {
|
||||
if (*count >= MAX_IDENTITY_MAP)
|
||||
return -1;
|
||||
IdentityMap* grown = realloc(*map, (size_t)(*count + 1) * sizeof(IdentityMap));
|
||||
if (!grown)
|
||||
return -1;
|
||||
*map = grown;
|
||||
(*map)[*count].from = from;
|
||||
(*map)[*count].to = to;
|
||||
(*map)[*count] = *rule;
|
||||
(*count)++;
|
||||
return 0;
|
||||
}
|
||||
@@ -326,7 +498,7 @@ int identity_parse_map(Config* config, const char* value, bool is_group) {
|
||||
char* saveptr = NULL;
|
||||
for (char* rule = strtok_r(list, ",", &saveptr); rule; rule = strtok_r(NULL, ",", &saveptr)) {
|
||||
char* colon = strchr(rule, ':');
|
||||
if (!colon || colon == rule) {
|
||||
if (!colon) {
|
||||
/* Log before freeing: `rule` points into the str_dup'd list. */
|
||||
log_message(LOG_LEVEL_ERROR, "%s rules must be FROM:TO (got '%s')", optname, rule);
|
||||
free(list);
|
||||
@@ -335,25 +507,25 @@ int identity_parse_map(Config* config, const char* value, bool is_group) {
|
||||
*colon = '\0';
|
||||
char* from_token = rule;
|
||||
char* to_token = colon + 1;
|
||||
if (*to_token == '\0') {
|
||||
IdentityMap parsed;
|
||||
memset(&parsed, 0, sizeof(parsed));
|
||||
if (identity_parse_from(from_token, is_group, &parsed.from, &parsed.from_hi) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"%s could not resolve FROM '%s' in '%s' (a name must exist on the "
|
||||
"source; use @N for a numeric id)",
|
||||
optname, from_token, value);
|
||||
free(list);
|
||||
log_message(LOG_LEVEL_ERROR, "%s rule 'FROM:' is missing the TO value (got '%s')", optname,
|
||||
value);
|
||||
return -1;
|
||||
}
|
||||
int32_t from_id, to_id;
|
||||
if (identity_resolve_token(from_token, is_group, &from_id) != 0 ||
|
||||
identity_resolve_token(to_token, is_group, &to_id) != 0) {
|
||||
if (identity_parse_to(to_token, is_group, &parsed.to, &parsed.to_name) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "%s could not parse TO '%s' in '%s'", optname, to_token, value);
|
||||
free(list);
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"%s could not resolve '%s' (name must exist on the source; use "
|
||||
"@N for a numeric id)",
|
||||
optname, value);
|
||||
return -1;
|
||||
}
|
||||
if (identity_append_rule(is_group ? &config->groupmap : &config->usermap,
|
||||
is_group ? &config->groupmap_count : &config->usermap_count, from_id,
|
||||
to_id) != 0) {
|
||||
is_group ? &config->groupmap_count : &config->usermap_count,
|
||||
&parsed) != 0) {
|
||||
free(parsed.to_name);
|
||||
free(list);
|
||||
log_message(LOG_LEVEL_ERROR, "%s has too many rules (max %d)", optname, MAX_IDENTITY_MAP);
|
||||
return -1;
|
||||
@@ -628,17 +800,108 @@ done:
|
||||
|
||||
/* ---- Receiver-side ownership application ---- */
|
||||
|
||||
static bool identity_map_lookup(const IdentityMap* map, int count, int32_t source_id,
|
||||
/* True when a map rule's FROM matcher accepts `id`. A sentinel FROM never
|
||||
* carries a range. IDENTITY_MATCH_UNNAMED mirrors rsync's empty FROM: it
|
||||
* matches only ids that have no name in the account database (rsync matches the
|
||||
* sender's names; FastSync transmits numeric ids only, so it approximates this
|
||||
* with the receiver's database -- documented in RSYNC_COMPAT.md). */
|
||||
static bool identity_map_from_matches(const IdentityMap* map, int32_t id, bool is_group) {
|
||||
if (map->from == IDENTITY_MATCH_ANY)
|
||||
return true;
|
||||
if (map->from == IDENTITY_MATCH_UNNAMED)
|
||||
return is_group ? (getgrgid((gid_t)id) == NULL) : (getpwuid((uid_t)id) == NULL);
|
||||
return id >= map->from && id <= map->from_hi;
|
||||
}
|
||||
|
||||
/* First matching rule wins. A rule whose TO is a receiver-side name resolves it
|
||||
* against the receiver's account database here; an unresolvable TO name is
|
||||
* skipped with a warning and the next rule is considered (rsync prints "Unknown
|
||||
* --usermap name on receiver" and leaves the id unmapped rather than aborting). */
|
||||
static bool identity_map_lookup(const IdentityMap* map, int count, int32_t source_id, bool is_group,
|
||||
int32_t* out_to) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (map[i].from == IDENTITY_MATCH_ANY || map[i].from == source_id) {
|
||||
if (!identity_map_from_matches(&map[i], source_id, is_group))
|
||||
continue;
|
||||
if (map[i].to_name) {
|
||||
if (is_group) {
|
||||
struct group* gr = getgrnam(map[i].to_name);
|
||||
if (!gr) {
|
||||
log_message(LOG_LEVEL_WARNING, "Unknown --groupmap name on receiver: %s", map[i].to_name);
|
||||
continue;
|
||||
}
|
||||
*out_to = (int32_t)gr->gr_gid;
|
||||
} else {
|
||||
struct passwd* pw = getpwnam(map[i].to_name);
|
||||
if (!pw) {
|
||||
log_message(LOG_LEVEL_WARNING, "Unknown --usermap name on receiver: %s", map[i].to_name);
|
||||
continue;
|
||||
}
|
||||
*out_to = (int32_t)pw->pw_uid;
|
||||
}
|
||||
} else {
|
||||
*out_to = map[i].to;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Resolve the owner side from the negotiated policy. Sets *out and returns
|
||||
* true when an owner-affecting request is active (a usermap, --chown USER, or
|
||||
* -o/--owner); returns false (leaving *out untouched) when the owner side is
|
||||
* not requested, so callers can pass (uid_t)-1 to fchown and leave it as-is.
|
||||
* --numeric-ids only changes the RESOLUTION (raw id instead of a name lookup);
|
||||
* it never makes the side requested. */
|
||||
static bool identity_resolve_owner(int32_t source_uid, uid_t* out) {
|
||||
if (!(g_identity.chown_uid_set || g_identity.preserve_owner || g_identity.usermap_count > 0))
|
||||
return false;
|
||||
int32_t target;
|
||||
if (identity_map_lookup(g_identity.usermap, g_identity.usermap_count, source_uid, false,
|
||||
&target)) {
|
||||
*out = target == IDENTITY_CURRENT ? geteuid() : (uid_t)target;
|
||||
} else if (g_identity.chown_uid_set) {
|
||||
*out = g_identity.chown_uid == IDENTITY_CURRENT ? geteuid() : (uid_t)g_identity.chown_uid;
|
||||
} else if (g_identity.numeric_ids) {
|
||||
*out = (uid_t)source_uid;
|
||||
} else {
|
||||
/* Best-effort name mapping against the receiver's own database. When the
|
||||
* transmitted (numeric) id has no name here, fall back to the raw numeric id
|
||||
* so -o still preserves the source owner. */
|
||||
struct passwd* pw = getpwuid((uid_t)source_uid);
|
||||
if (pw) {
|
||||
const struct passwd* mapped = getpwnam(pw->pw_name);
|
||||
*out = mapped ? mapped->pw_uid : (uid_t)source_uid;
|
||||
} else {
|
||||
*out = (uid_t)source_uid;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Group-side counterpart of identity_resolve_owner(). */
|
||||
static bool identity_resolve_group(int32_t source_gid, gid_t* out) {
|
||||
if (!(g_identity.chown_gid_set || g_identity.preserve_group || g_identity.groupmap_count > 0))
|
||||
return false;
|
||||
int32_t target;
|
||||
if (identity_map_lookup(g_identity.groupmap, g_identity.groupmap_count, source_gid, true,
|
||||
&target)) {
|
||||
*out = target == IDENTITY_CURRENT ? getegid() : (gid_t)target;
|
||||
} else if (g_identity.chown_gid_set) {
|
||||
*out = g_identity.chown_gid == IDENTITY_CURRENT ? getegid() : (gid_t)g_identity.chown_gid;
|
||||
} else if (g_identity.numeric_ids) {
|
||||
*out = (gid_t)source_gid;
|
||||
} else {
|
||||
struct group* gr = getgrgid((gid_t)source_gid);
|
||||
if (gr) {
|
||||
const struct group* mapped = getgrnam(gr->gr_name);
|
||||
*out = mapped ? mapped->gr_gid : (gid_t)source_gid;
|
||||
} else {
|
||||
*out = (gid_t)source_gid;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Resolve the target ownership from the negotiated policy against the entry's
|
||||
* current stat. Shared by the fd (regular file) and no-follow (symlink) apply
|
||||
* paths. Returns false when no side is to be changed. */
|
||||
@@ -662,58 +925,12 @@ static bool identity_resolve_targets(const struct stat* st, int32_t source_uid,
|
||||
* request the owner/group respectively, and a side that is NOT requested must
|
||||
* be left exactly as it is (`-1` to fchown on that side). This is what lets
|
||||
* plain -g change only the group, or -o only the owner. */
|
||||
bool owner_requested = g_identity.chown_uid_set || g_identity.numeric_ids ||
|
||||
g_identity.preserve_owner || g_identity.usermap_count > 0;
|
||||
bool group_requested = g_identity.chown_gid_set || g_identity.numeric_ids ||
|
||||
g_identity.preserve_group || g_identity.groupmap_count > 0;
|
||||
if (!owner_requested && !group_requested)
|
||||
return false;
|
||||
|
||||
int32_t target;
|
||||
uid_t uid = (uid_t)-1;
|
||||
gid_t gid = (gid_t)-1;
|
||||
|
||||
/* Priority (unchanged): usermap/groupmap > --chown > --numeric-ids (raw) >
|
||||
* name mapping on the transmitted numeric id, with a raw-id fallback when the
|
||||
* receiver has no name for that id. */
|
||||
if (owner_requested) {
|
||||
if (identity_map_lookup(g_identity.usermap, g_identity.usermap_count, source_uid, &target)) {
|
||||
uid = target == IDENTITY_CURRENT ? geteuid() : (uid_t)target;
|
||||
} else if (g_identity.chown_uid_set) {
|
||||
uid = g_identity.chown_uid == IDENTITY_CURRENT ? geteuid() : (uid_t)g_identity.chown_uid;
|
||||
} else if (g_identity.numeric_ids) {
|
||||
uid = (uid_t)source_uid;
|
||||
} else {
|
||||
/* Best-effort name mapping against the receiver's own database. When the
|
||||
* transmitted (numeric) id has no name here, fall back to the raw numeric
|
||||
* id so -o still preserves the source owner. */
|
||||
struct passwd* pw = getpwuid((uid_t)source_uid);
|
||||
if (pw) {
|
||||
const struct passwd* mapped = getpwnam(pw->pw_name);
|
||||
uid = mapped ? mapped->pw_uid : (uid_t)source_uid;
|
||||
} else {
|
||||
uid = (uid_t)source_uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (group_requested) {
|
||||
if (identity_map_lookup(g_identity.groupmap, g_identity.groupmap_count, source_gid, &target)) {
|
||||
gid = target == IDENTITY_CURRENT ? getegid() : (gid_t)target;
|
||||
} else if (g_identity.chown_gid_set) {
|
||||
gid = g_identity.chown_gid == IDENTITY_CURRENT ? getegid() : (gid_t)g_identity.chown_gid;
|
||||
} else if (g_identity.numeric_ids) {
|
||||
gid = (gid_t)source_gid;
|
||||
} else {
|
||||
struct group* gr = getgrgid((gid_t)source_gid);
|
||||
if (gr) {
|
||||
const struct group* mapped = getgrnam(gr->gr_name);
|
||||
gid = mapped ? mapped->gr_gid : (gid_t)source_gid;
|
||||
} else {
|
||||
gid = (gid_t)source_gid;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool owner_requested = identity_resolve_owner(source_uid, &uid);
|
||||
bool group_requested = identity_resolve_group(source_gid, &gid);
|
||||
if (!owner_requested && !group_requested)
|
||||
return false;
|
||||
|
||||
/* Only change ownership when a requested side actually differs (avoid
|
||||
* needless syscalls and any chance of clearing setuid/setgid on an
|
||||
@@ -726,6 +943,30 @@ static bool identity_resolve_targets(const struct stat* st, int32_t source_uid,
|
||||
return true;
|
||||
}
|
||||
|
||||
/* --fake-super storage resolution: the receiver records the ownership it WOULD
|
||||
* have applied. A requested side uses the resolved mapping (--copy-as /
|
||||
* usermap / --chown / -o/-g, with --numeric-ids as the raw-id modifier); a side
|
||||
* that was not requested keeps the source's own id, so a plain --fake-super run
|
||||
* records the source owner untouched. */
|
||||
void identity_resolve_storage_ids(int32_t source_uid, int32_t source_gid, uint32_t* out_uid,
|
||||
uint32_t* out_gid) {
|
||||
if (g_identity.copy_as_set) {
|
||||
*out_uid = (uint32_t)g_identity.copy_as_uid;
|
||||
*out_gid = (uint32_t)g_identity.copy_as_gid;
|
||||
return;
|
||||
}
|
||||
uid_t uid = (uid_t)source_uid;
|
||||
gid_t gid = (gid_t)source_gid;
|
||||
uid_t resolved_uid;
|
||||
gid_t resolved_gid;
|
||||
if (identity_resolve_owner(source_uid, &resolved_uid))
|
||||
uid = resolved_uid;
|
||||
if (identity_resolve_group(source_gid, &resolved_gid))
|
||||
gid = resolved_gid;
|
||||
*out_uid = (uint32_t)uid;
|
||||
*out_gid = (uint32_t)gid;
|
||||
}
|
||||
|
||||
static void identity_log_chown_failure(const char* what, uid_t uid, gid_t gid) {
|
||||
/* EPERM/EACCES are expected when the receiver is not privileged (e.g. the CI
|
||||
* `nobody` user): warn and continue, never abort the transfer. Any other
|
||||
@@ -760,8 +1001,12 @@ bool identity_apply_ownership(int fd, int32_t source_uid, int32_t source_gid) {
|
||||
/* Ownership application is OFF unless the client requested an identity flag.
|
||||
* This is the controlled gate: a default (or plain -M) transfer never changes
|
||||
* ownership, byte-for-byte preserving FastSync's existing behavior. --no-super
|
||||
* additionally forbids it even when the receiver is root. */
|
||||
if (!identity_active_enabled() || !privilege_super_permitted() || fd < 0)
|
||||
* additionally forbids it even when the receiver is root. --fake-super never
|
||||
* performs a REAL chown: that would defeat the point of the flag (record the
|
||||
* source ownership on an unprivileged receiver for a later privileged
|
||||
* restore); the resolved ownership is stored in the reserved xattr instead by
|
||||
* fake_super_store_fd(). */
|
||||
if (!identity_active_enabled() || g_identity.fake_super || !privilege_super_permitted() || fd < 0)
|
||||
return true;
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) != 0)
|
||||
@@ -781,7 +1026,8 @@ bool identity_apply_ownership(int fd, int32_t source_uid, int32_t source_gid) {
|
||||
|
||||
bool identity_apply_ownership_link(int parent_fd, const char* leaf, int32_t source_uid,
|
||||
int32_t source_gid) {
|
||||
if (!identity_active_enabled() || !privilege_super_permitted() || parent_fd < 0 || !leaf)
|
||||
if (!identity_active_enabled() || g_identity.fake_super || !privilege_super_permitted() ||
|
||||
parent_fd < 0 || !leaf)
|
||||
return true;
|
||||
struct stat st;
|
||||
if (fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) != 0)
|
||||
|
||||
Reference in New Issue
Block a user