Merge branch 'feat/p7-copy-as' into feat/p7-privilege
# Conflicts: # RSYNC_COMPAT.md # src/shared/config.c # src/shared/config.h # src/shared/identity.c # tests/integration/test_preflight.py # tests/test_client_cli.c # tests/test_config.c
This commit is contained in:
@@ -1428,6 +1428,18 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
if (identity_parse_chown(config, argv[++i]) != 0)
|
||||
return -1;
|
||||
config->use_metadata = true;
|
||||
} else if (strncmp(argv[i], "--copy-as=", 10) == 0) {
|
||||
if (identity_parse_copy_as(config, argv[i] + 10) != 0)
|
||||
return -1;
|
||||
config->use_metadata = true;
|
||||
} else if (opt_is(argv[i], "--copy-as", NULL)) {
|
||||
if (i + 1 >= argc) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]);
|
||||
return -1;
|
||||
}
|
||||
if (identity_parse_copy_as(config, argv[++i]) != 0)
|
||||
return -1;
|
||||
config->use_metadata = true;
|
||||
} else if (strncmp(argv[i], "--outbuf=", 9) == 0) {
|
||||
if (set_outbuf_option(config, argv[i] + 9) != 0)
|
||||
return -1;
|
||||
|
||||
@@ -175,6 +175,16 @@ static const char* server_module_gate(const Config* config, void* context) {
|
||||
ModuleGateContext* gate_ctx = (ModuleGateContext*)context;
|
||||
if (!config)
|
||||
return "missing config frame";
|
||||
/* --copy-as (P7 Wave E, protocol 2.18.0): FastSync's safe subset forces the
|
||||
ownership of every written entry to the requested ids, which needs a
|
||||
privileged (root) receiver. An unprivileged receiver REFUSES the whole
|
||||
transfer here, at the config handshake and BEFORE the STATUS_OK ack, so no
|
||||
file data is exchanged and there is never a silent wrong-ownership result.
|
||||
Placed first so it applies to the standalone server and daemon alike. */
|
||||
if (config->copy_as_set && geteuid() != 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as requires a privileged receiver (root); refusing");
|
||||
return "--copy-as requires a privileged receiver (root)";
|
||||
}
|
||||
/* --iconv (protocol 2.16.0): the receiver's exact conversion direction (the
|
||||
client spec's wire charset into this server's local charset, including a
|
||||
server-side --iconv override) must be usable BEFORE the STATUS_OK ack, so
|
||||
|
||||
+41
-2
@@ -178,6 +178,9 @@ static void config_set_defaults(Config* config) {
|
||||
config->open_noatime = false;
|
||||
config->use_xattrs = false;
|
||||
config->fake_super = false;
|
||||
config->copy_as_set = false;
|
||||
config->copy_as_uid = 0;
|
||||
config->copy_as_gid = 0;
|
||||
config->trust_sender = false;
|
||||
config->stop_after_mins = 0;
|
||||
config->stop_at = 0;
|
||||
@@ -242,6 +245,7 @@ static bool validate_received_config(const Config* config) {
|
||||
valid_wire_bool(config->omit_dir_times) && valid_wire_bool(config->omit_link_times) &&
|
||||
valid_wire_bool(config->munge_links) && valid_wire_bool(config->keep_dirlinks) &&
|
||||
valid_wire_bool(config->fake_super) &&
|
||||
(!config->copy_as_set || (config->copy_as_uid >= 0 && config->copy_as_gid >= 0)) &&
|
||||
(!config->use_compression ||
|
||||
(config->compression_level >= 1 && config->compression_level <= 22)) &&
|
||||
config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE &&
|
||||
@@ -1208,6 +1212,38 @@ static bool receive_privilege_options(int fd, Config* c) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* --copy-as=USER[:GROUP] (P7 Wave E, protocol 2.18.0). Trailing block on the
|
||||
* config frame, sent after the --super int and before the ack: a presence int,
|
||||
* then (when set) the target uid and gid as int32. The receiver forces the
|
||||
* ownership of every entry it writes to these ids through the confined
|
||||
* fd-relative identity path and requires privilege; both ids are validated
|
||||
* `>= 0` on receive so a hostile peer cannot smuggle a negative (sentinel)
|
||||
* value into the ownership path. */
|
||||
static bool send_copy_as_options(int fd, const Config* c) {
|
||||
if (!send_int(fd, c->copy_as_set ? 1 : 0))
|
||||
return false;
|
||||
if (!c->copy_as_set)
|
||||
return true;
|
||||
return send_int(fd, c->copy_as_uid) && send_int(fd, c->copy_as_gid);
|
||||
}
|
||||
|
||||
static bool receive_copy_as_options(int fd, Config* c) {
|
||||
int present;
|
||||
if (!receive_int(fd, &present) || !valid_wire_bool(present))
|
||||
return false;
|
||||
if (!present) {
|
||||
c->copy_as_set = false;
|
||||
return true;
|
||||
}
|
||||
int uid, gid;
|
||||
if (!receive_int(fd, &uid) || !receive_int(fd, &gid) || uid < 0 || gid < 0)
|
||||
return false;
|
||||
c->copy_as_set = true;
|
||||
c->copy_as_uid = uid;
|
||||
c->copy_as_gid = gid;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool config_send(int file_descriptor, const Config* config) {
|
||||
protocol_session_set_max_alloc(NULL, config->max_alloc);
|
||||
if (!send_core_fields(file_descriptor, config) || !send_delta_fields(file_descriptor, config) ||
|
||||
@@ -1221,7 +1257,9 @@ bool config_send(int file_descriptor, const Config* config) {
|
||||
!send_symlink_trust_options(file_descriptor, config) ||
|
||||
!send_phase4_xattr_options(file_descriptor, config) ||
|
||||
!send_daemon_module(file_descriptor, config) || !send_daemon_auth(file_descriptor, config) ||
|
||||
!send_iconv_spec(file_descriptor, config) || !send_privilege_options(file_descriptor, config))
|
||||
!send_iconv_spec(file_descriptor, config) ||
|
||||
!send_privilege_options(file_descriptor, config) ||
|
||||
!send_copy_as_options(file_descriptor, config))
|
||||
return false;
|
||||
Status status;
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
@@ -1265,7 +1303,8 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
|
||||
!receive_daemon_module(file_descriptor, config) ||
|
||||
!receive_daemon_auth(file_descriptor, config) ||
|
||||
!receive_iconv_spec(file_descriptor, config) ||
|
||||
!receive_privilege_options(file_descriptor, config))
|
||||
!receive_privilege_options(file_descriptor, config) ||
|
||||
!receive_copy_as_options(file_descriptor, config))
|
||||
goto error;
|
||||
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 &&
|
||||
strcmp(config->compress_choice, "none") != 0) {
|
||||
|
||||
+38
-17
@@ -455,6 +455,20 @@ typedef struct Config {
|
||||
* a reserved user.fastsync.stat xattr recording the source uid/gid/mode/mtime
|
||||
* so a later privileged restore could re-apply them. Crosses the wire. */
|
||||
bool fake_super;
|
||||
/* --copy-as=USER[:GROUP] (P7 Wave E, protocol 2.18.0). Safe-subset
|
||||
* implementation, a documented divergence from rsync's real identity switch:
|
||||
* the receiver does NOT change its process credentials (FastSync's receiver
|
||||
* is multithreaded, so a setuid/seteuid drop would be unsafe). Instead the
|
||||
* receiver FORCES the ownership of every entry it writes to copy_as_uid /
|
||||
* copy_as_gid through the existing confined, fd-relative identity path
|
||||
* (fchown/fchownat), which REQUIRES receiver privilege (root); an
|
||||
* unprivileged receiver REFUSES the whole transfer up front at the config
|
||||
* handshake (never a silent wrong-ownership result). All three fields CROSS
|
||||
* the wire as a trailing config-frame block so the receiver learns the
|
||||
* requested ids; see the PROTOCOL_VERSION note below. */
|
||||
bool copy_as_set;
|
||||
int32_t copy_as_uid;
|
||||
int32_t copy_as_gid;
|
||||
|
||||
// Phase 5: --trust-sender
|
||||
/* Long-form-only, receiver-local policy. rsync's --trust-sender tells the
|
||||
@@ -584,18 +598,25 @@ typedef struct Config {
|
||||
* Privilege Wave (P7 Wave E): 2.17.0 -> 2.18.0.
|
||||
*
|
||||
* WHY the bump, grounded in the wire: this wave adds the receiver-side
|
||||
* --super / --no-super privilege policy. The config-frame layout gains a new
|
||||
* trailing int (Config->super_mode) sent immediately AFTER the --iconv
|
||||
* CONVERT_SPEC block (send_privilege_options / receive_privilege_options in
|
||||
* config.c), so the receiver knows whether it may attempt super-user
|
||||
* activities (ownership application, char/block device-node creation) that are
|
||||
* already confined below the authorized receive root. Any config-frame layout
|
||||
* change must bump the protocol version: a peer that does not parse the new
|
||||
* trailing bytes would desynchronize on the frame boundary, and the strict
|
||||
* same-version handshake (config_receive rejects a mismatched version before
|
||||
* parsing anything else) is what keeps a 2.18 client and a 2.17 server from
|
||||
* ever reaching that state. --super never elevates privileges; it only
|
||||
* permits a confined attempt, so no new capability is granted. */
|
||||
* privilege flags --super/--no-super and --copy-as=USER[:GROUP]. The
|
||||
* config-frame layout gains two new trailing blocks AFTER the --iconv
|
||||
* CONVERT_SPEC string, in this fixed order: (1) send_privilege_options /
|
||||
* receive_privilege_options send one int (Config->super_mode, 0..2), then
|
||||
* (2) send_copy_as_options / receive_copy_as_options send a presence int and,
|
||||
* when set, the target uid and gid (both int32). The receiver uses
|
||||
* super_mode to decide whether it may attempt super-user activities
|
||||
* (ownership application, char/block device-node creation) already confined
|
||||
* below the authorized receive root, and the copy-as ids to force the
|
||||
* ownership of every entry it writes (the safe-subset --copy-as model). The
|
||||
* receiver REQUIRES privilege for copy-as: an unprivileged receiver refuses
|
||||
* the transfer at the config handshake (server_module_gate) instead of silently
|
||||
* ignoring the flag. Any config-frame layout change must bump the protocol
|
||||
* version: a peer that does not parse the new trailing bytes would
|
||||
* desynchronize on the frame boundary, and the strict same-version handshake
|
||||
* (config_receive rejects a mismatched version before parsing anything else) is
|
||||
* what keeps a 2.18 client and a 2.17 server from ever reaching that state.
|
||||
* --super never elevates privileges; it only permits a confined attempt, and
|
||||
* --copy-as never switches process credentials (see RSYNC_COMPAT.md). */
|
||||
#define PROTOCOL_VERSION "2.18.0"
|
||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
||||
@@ -609,11 +630,11 @@ typedef struct Config {
|
||||
#define IDENTITY_CURRENT (-1)
|
||||
#define MAX_IDENTITY_MAP 128
|
||||
|
||||
/* --super / --no-super tri-state (Config->super_mode). AUTO preserves the
|
||||
* pre-existing behavior (a privileged attempt only when already root); ON
|
||||
* permits confined privileged attempts; OFF forbids them even as root. See the
|
||||
* Config->super_mode comment above and privilege_super_permitted() in
|
||||
* identity.h. */
|
||||
/* --super / --no-super tri-state (Config->super_mode). AUTO (default) and ON
|
||||
* both permit a confined super-user attempt (AUTO preserves FastSync's
|
||||
* historical best-effort behavior; an unprivileged attempt is refused by the
|
||||
* kernel and skipped per entry); OFF forbids the attempt even for root. See
|
||||
* privilege_super_mode_permitted() in identity.h. */
|
||||
#define SUPER_MODE_AUTO 0
|
||||
#define SUPER_MODE_ON 1
|
||||
#define SUPER_MODE_OFF 2
|
||||
|
||||
+128
-16
@@ -31,6 +31,11 @@ typedef struct {
|
||||
* per connection so privilege_super_permitted() can gate super-user
|
||||
* activities without a Config argument. */
|
||||
int super_mode;
|
||||
/* --copy-as=USER[:GROUP]: snapshotted so the ownership resolver can force the
|
||||
* target ids without a Config argument. */
|
||||
bool copy_as_set;
|
||||
int32_t copy_as_uid;
|
||||
int32_t copy_as_gid;
|
||||
bool set;
|
||||
} IdentityActive;
|
||||
|
||||
@@ -49,6 +54,9 @@ static void identity_active_reset(void) {
|
||||
g_identity.chown_gid_set = false;
|
||||
g_identity.chown_gid = 0;
|
||||
g_identity.super_mode = SUPER_MODE_AUTO;
|
||||
g_identity.copy_as_set = false;
|
||||
g_identity.copy_as_uid = 0;
|
||||
g_identity.copy_as_gid = 0;
|
||||
g_identity.set = false;
|
||||
}
|
||||
|
||||
@@ -65,6 +73,10 @@ void identity_set_active(const Config* config) {
|
||||
g_identity.chown_uid = config->chown_uid;
|
||||
g_identity.chown_gid_set = config->chown_gid_set;
|
||||
g_identity.chown_gid = config->chown_gid;
|
||||
g_identity.super_mode = config->super_mode;
|
||||
g_identity.copy_as_set = config->copy_as_set;
|
||||
g_identity.copy_as_uid = config->copy_as_uid;
|
||||
g_identity.copy_as_gid = config->copy_as_gid;
|
||||
if (config->usermap_count > 0) {
|
||||
g_identity.usermap = calloc((size_t)config->usermap_count, sizeof(IdentityMap));
|
||||
if (g_identity.usermap) {
|
||||
@@ -84,33 +96,37 @@ void identity_set_active(const Config* config) {
|
||||
g_identity.super_mode = config->super_mode;
|
||||
g_identity.set = true;
|
||||
/* A root receiver would honor any client-supplied ownership request (a
|
||||
--usermap/--groupmap/--chown, or raw ids under --numeric-ids). Surface
|
||||
that prominently; a privileged daemon applying arbitrary client ownership
|
||||
is a deliberate, opt-in choice the operator should be aware of. */
|
||||
--usermap/--groupmap/--chown/--copy-as, or raw ids under --numeric-ids).
|
||||
Surface that prominently; a privileged daemon applying arbitrary client
|
||||
ownership is a deliberate, opt-in choice the operator should be aware of. */
|
||||
if (geteuid() == 0)
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"identity mapping active and running as root: client-supplied "
|
||||
"ownership (usermap/groupmap/chown/numeric-ids) will be honored; "
|
||||
"run the daemon as an unprivileged user unless intended");
|
||||
/* --super explicitly requests super-user activities, but FastSync never
|
||||
elevates privileges: when the receiver is not already root those confined
|
||||
attempts cannot succeed. Warn exactly once at activation time (never
|
||||
abort) so the operator knows the flag is inert on this host. */
|
||||
elevates privileges: when the receiver is not already root the kernel will
|
||||
refuse those confined attempts and each is skipped per entry. Warn exactly
|
||||
once at activation time (never abort) so the operator knows the flag cannot
|
||||
succeed on this host. */
|
||||
if (g_identity.super_mode == SUPER_MODE_ON && geteuid() != 0)
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"--super requested but the receiver is not privileged; super-user "
|
||||
"activities (ownership, device nodes) cannot be performed and will "
|
||||
"be skipped");
|
||||
"activities (ownership, device nodes) will be attempted but refused "
|
||||
"by the kernel and skipped per entry");
|
||||
}
|
||||
|
||||
bool privilege_super_permitted(void) {
|
||||
if (g_identity.super_mode == SUPER_MODE_OFF)
|
||||
return false;
|
||||
if (g_identity.super_mode == SUPER_MODE_ON)
|
||||
return true;
|
||||
/* SUPER_MODE_AUTO (the default): only attempt super-user activities when the
|
||||
receiver is already root. */
|
||||
return geteuid() == 0;
|
||||
return privilege_super_mode_permitted(g_identity.super_mode);
|
||||
}
|
||||
|
||||
bool privilege_super_mode_permitted(int mode) {
|
||||
/* AUTO and ON both attempt the confined operation; OFF forbids it even for a
|
||||
* root receiver. AUTO is the historical FastSync behavior (always attempt
|
||||
* and let the kernel refuse an unprivileged call, which the caller skips), so
|
||||
* it must stay permissive or a group-only chown that a non-root receiver is
|
||||
* allowed to make would regress. */
|
||||
return mode != SUPER_MODE_OFF;
|
||||
}
|
||||
|
||||
/* --super with NO explicit identity policy implies raw numeric-id preservation,
|
||||
@@ -134,7 +150,8 @@ bool identity_active_enabled(void) {
|
||||
with no explicit identity policy acts like --numeric-ids here. */
|
||||
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 || identity_super_implies_numeric());
|
||||
g_identity.groupmap_count > 0 || g_identity.copy_as_set ||
|
||||
identity_super_implies_numeric());
|
||||
}
|
||||
|
||||
bool identity_wire_valid(const Config* config) {
|
||||
@@ -396,6 +413,87 @@ done:
|
||||
return ret;
|
||||
}
|
||||
|
||||
int identity_parse_copy_as(Config* config, const char* value) {
|
||||
if (!config || !value || *value == '\0') {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as requires USER[:GROUP]");
|
||||
return -1;
|
||||
}
|
||||
/* --copy-as=USER[:GROUP] is the whole grammar: at most one field separator.
|
||||
* (Unlike --chown there is no escaped-colon form; a name containing ':' is
|
||||
* simply not expressible, and the extra colon is a clear parse error.) */
|
||||
int colons = 0;
|
||||
for (const char* p = value; *p; p++)
|
||||
if (*p == ':')
|
||||
colons++;
|
||||
if (colons > 1) {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as must be USER[:GROUP] (got '%s')", value);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* spec = str_dup(value);
|
||||
if (!spec) {
|
||||
log_message(LOG_LEVEL_ERROR, "memory allocation failed for --copy-as");
|
||||
return -1;
|
||||
}
|
||||
char* user_token = spec;
|
||||
char* group_token = NULL;
|
||||
char* colon = strchr(spec, ':');
|
||||
if (colon) {
|
||||
*colon = '\0';
|
||||
group_token = colon + 1;
|
||||
}
|
||||
|
||||
int32_t uid;
|
||||
if (*user_token == '\0') {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as is missing the user (got '%s')", value);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
if (strcmp(user_token, "*") == 0) {
|
||||
/* '*' means the current/root user: the client's euid. */
|
||||
uid = (int32_t)geteuid();
|
||||
} else if (identity_resolve_token(user_token, false, &uid) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--copy-as could not resolve user '%s' (use a name that exists "
|
||||
"on the source, '*', or @N)",
|
||||
value);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int32_t gid;
|
||||
if (group_token) {
|
||||
if (*group_token == '\0') {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as group is empty (got '%s')", value);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
if (strcmp(group_token, "*") == 0) {
|
||||
gid = (int32_t)getegid();
|
||||
} else if (identity_resolve_token(group_token, true, &gid) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as could not resolve group '%s' (got '%s')", group_token,
|
||||
value);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
/* Group omitted: use the user's primary gid. A numeric id with no local
|
||||
* passwd entry has no primary gid to look up, so fall back to gid == uid
|
||||
* (the rsync-style numeric convention; documented divergence). */
|
||||
struct passwd* pw = getpwuid((uid_t)uid);
|
||||
gid = pw ? (int32_t)pw->pw_gid : uid;
|
||||
}
|
||||
free(spec);
|
||||
|
||||
config->copy_as_set = true;
|
||||
config->copy_as_uid = uid;
|
||||
config->copy_as_gid = gid;
|
||||
/* Ownership application needs the metadata path (the source uid/gid must be
|
||||
* transmitted); imply it exactly like --chown/--usermap/--groupmap. */
|
||||
config->use_metadata = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- Receiver-side ownership application ---- */
|
||||
|
||||
static bool identity_map_lookup(const IdentityMap* map, int count, int32_t source_id,
|
||||
@@ -419,6 +517,20 @@ static bool identity_resolve_targets(const struct stat* st, int32_t source_uid,
|
||||
uid_t uid = 0;
|
||||
gid_t gid = 0;
|
||||
|
||||
/* --copy-as (P7 Wave E) has the highest priority: it forces BOTH the owner
|
||||
* and group of every written entry to the requested ids, beating usermap /
|
||||
* groupmap / --chown / --numeric-ids and the best-effort name lookup. Only
|
||||
* skip when the entry already carries exactly those ids. */
|
||||
if (g_identity.copy_as_set) {
|
||||
uid = (uid_t)g_identity.copy_as_uid;
|
||||
gid = (gid_t)g_identity.copy_as_gid;
|
||||
if (st->st_uid == uid && st->st_gid == gid)
|
||||
return false;
|
||||
*out_uid = uid;
|
||||
*out_gid = gid;
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t target;
|
||||
if (identity_map_lookup(g_identity.usermap, g_identity.usermap_count, source_uid, &target)) {
|
||||
uid = target == IDENTITY_CURRENT ? geteuid() : (uid_t)target;
|
||||
|
||||
+31
-7
@@ -34,6 +34,28 @@ int identity_parse_map(Config* config, const char* value, bool is_group);
|
||||
* on success, -1 on a malformed spec / unresolvable name. */
|
||||
int identity_parse_chown(Config* config, const char* value);
|
||||
|
||||
/* Parse --copy-as=USER[:GROUP] (P7 Wave E). USER is resolved with the same
|
||||
* user-database rules as --chown (a name, @N/bare N numeric id, or '*' meaning
|
||||
* the client's current euid); when ':GROUP' is present the group is resolved
|
||||
* with the group database ('*' meaning the client's egid). When the group is
|
||||
* omitted, the user's primary gid is used (getpwuid(uid)->pw_gid); if the
|
||||
* resolved user is a numeric id with no local passwd entry, gid falls back to
|
||||
* uid. On success sets copy_as_set/copy_as_uid/copy_as_gid and forces
|
||||
* metadata transmission (ownership application needs the metadata path).
|
||||
* Returns 0 on success, -1 on a malformed / empty / unresolvable spec (never a
|
||||
* silent no-op). */
|
||||
int identity_parse_copy_as(Config* config, const char* value);
|
||||
|
||||
/* True when a --copy-as request is active but the receiver is not permitted to
|
||||
* perform the privileged ownership application it needs. This is the up-front
|
||||
* refusal predicate: the server rejects the whole transfer at the config
|
||||
* handshake rather than silently ignoring the requested ownership. It is a
|
||||
* pure function of the config mode and the current effective uid (it does NOT
|
||||
* read the active snapshot, so it is valid at the pre-STATUS_OK gate, before
|
||||
* identity_set_active() has run). `super_mode` is the EFFECTIVE mode after any
|
||||
* server-side policy veto. */
|
||||
bool identity_copy_as_refused(const Config* config);
|
||||
|
||||
/* Receiver-side snapshot of the negotiated identity config. The server calls
|
||||
* identity_set_active() once per connection (before any file write) using the
|
||||
* config received over the wire; the snapshot is a deep copy so the caller may
|
||||
@@ -66,13 +88,15 @@ void identity_apply_ownership_link(int parent_fd, const char* leaf, int32_t sour
|
||||
bool identity_wire_valid(const Config* config);
|
||||
|
||||
/* P7 Wave E receiver-side permission gate for super-user activities (ownership
|
||||
* application and char/block device-node creation). Returns false when the
|
||||
* active config is --no-super (SUPER_MODE_OFF); true when it is --super
|
||||
* (SUPER_MODE_ON); and otherwise (SUPER_MODE_AUTO, the default, or before
|
||||
* identity_set_active() has been called) only when the receiver is ALREADY root
|
||||
* (geteuid() == 0). This NEVER elevates privileges: it only reports whether an
|
||||
* attempt that is already confined below the authorized receive root may be
|
||||
* made. */
|
||||
* application and char/block device-node creation). `privilege_super_permitted`
|
||||
* consults the per-connection snapshot (call identity_set_active() first);
|
||||
* `privilege_super_mode_permitted` is the pure mode predicate and is what
|
||||
* callers holding a Config use (the config-frame gate, file_receive). Both
|
||||
* return false only for SUPER_MODE_OFF; SUPER_MODE_ON and SUPER_MODE_AUTO (the
|
||||
* default) permit a confined attempt, matching FastSync's historical
|
||||
* best-effort behavior where an unprivileged attempt is refused by the kernel
|
||||
* and skipped. Neither EVER elevates privileges. */
|
||||
bool privilege_super_permitted(void);
|
||||
bool privilege_super_mode_permitted(int mode);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user