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:
@@ -848,6 +848,20 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
config->no_motd = true;
|
||||
continue;
|
||||
}
|
||||
/* "--super" / "--no-super" are real rsync option names controlling the
|
||||
* receiver's super-user activity policy (ownership, device nodes), not a
|
||||
* Boolean pair for the generic --no-* negation branch: both map onto the
|
||||
* Config->super_mode tri-state. Handle them explicitly (exact match only,
|
||||
* so a malformed "--super=x" still falls through to the unknown-option
|
||||
* error) before the generic negation branch would mis-reject "--no-super". */
|
||||
if (strcmp(argv[i], "--super") == 0) {
|
||||
config->super_mode = SUPER_MODE_ON;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(argv[i], "--no-super") == 0) {
|
||||
config->super_mode = SUPER_MODE_OFF;
|
||||
continue;
|
||||
}
|
||||
if (strncmp(argv[i], "--no-", strlen("--no-")) == 0) {
|
||||
if (strcmp(argv[i], "--no-delta") == 0)
|
||||
no_delta = true;
|
||||
|
||||
@@ -168,6 +168,14 @@ void print_usage(void) {
|
||||
printf(" user.fastsync.stat xattr on each written file and\n");
|
||||
printf(" re-apply it (fd-relative) on a privileged run; the\n");
|
||||
printf(" recording format diverges from rsync's user.rsync.%%stat%%\n");
|
||||
printf(" --super Permit the receiver to attempt super-user activities\n");
|
||||
printf(" (ownership application, char/block device-node\n");
|
||||
printf(" creation) within the confined receive root. Never\n");
|
||||
printf(" elevates privileges and never bypasses confinement;\n");
|
||||
printf(" with no explicit identity policy, ownership follows\n");
|
||||
printf(" raw numeric ids (as if --numeric-ids)\n");
|
||||
printf(" --no-super Forbid those super-user activities even when the\n");
|
||||
printf(" receiver is running as root\n");
|
||||
printf(" --chmod <changes> Modify transferred permissions (rsync syntax)\n");
|
||||
printf(" --numeric-ids Do not map uid/gid by name: use the source numeric\n");
|
||||
printf(" ids directly when applying ownership\n");
|
||||
|
||||
+28
-3
@@ -169,6 +169,7 @@ static void config_set_defaults(Config* config) {
|
||||
config->usermap_count = 0;
|
||||
config->groupmap = NULL;
|
||||
config->groupmap_count = 0;
|
||||
config->super_mode = SUPER_MODE_AUTO;
|
||||
config->delay_context = NULL;
|
||||
config->preserve_atimes = false;
|
||||
config->preserve_crtimes = false;
|
||||
@@ -256,7 +257,10 @@ static bool validate_received_config(const Config* config) {
|
||||
unsupported charset name so the run is refused up front instead of
|
||||
every received file name failing mid-transfer. A NULL spec (iconv
|
||||
disabled) is always accepted. */
|
||||
(!config->iconv_spec || charset_spec_valid(config->iconv_spec));
|
||||
(!config->iconv_spec || charset_spec_valid(config->iconv_spec)) &&
|
||||
/* --super / --no-super: the received tri-state must be one of the
|
||||
defined values (AUTO/ON/OFF); anything else is a malformed frame. */
|
||||
config->super_mode >= SUPER_MODE_AUTO && config->super_mode <= SUPER_MODE_OFF;
|
||||
}
|
||||
|
||||
Config* config_create(void) {
|
||||
@@ -1185,6 +1189,25 @@ static bool receive_iconv_spec(int fd, Config* c) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* --super / --no-super privilege policy (P7 Wave E, protocol 2.18.0). One
|
||||
* trailing int on the config frame, sent after the --iconv spec and before the
|
||||
* STATUS_OK ack, 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. The received value is
|
||||
* validated to the SUPER_MODE_AUTO..SUPER_MODE_OFF range (also re-checked by
|
||||
* validate_received_config). */
|
||||
static bool send_privilege_options(int fd, const Config* c) {
|
||||
return send_int(fd, c->super_mode);
|
||||
}
|
||||
|
||||
static bool receive_privilege_options(int fd, Config* c) {
|
||||
int mode;
|
||||
if (!receive_int(fd, &mode) || mode < SUPER_MODE_AUTO || mode > SUPER_MODE_OFF)
|
||||
return false;
|
||||
c->super_mode = mode;
|
||||
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) ||
|
||||
@@ -1198,7 +1221,7 @@ 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_iconv_spec(file_descriptor, config) || !send_privilege_options(file_descriptor, config))
|
||||
return false;
|
||||
Status status;
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
@@ -1240,7 +1263,9 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
|
||||
!receive_symlink_trust_options(file_descriptor, config) ||
|
||||
!receive_phase4_xattr_options(file_descriptor, config) ||
|
||||
!receive_daemon_module(file_descriptor, config) ||
|
||||
!receive_daemon_auth(file_descriptor, config) || !receive_iconv_spec(file_descriptor, config))
|
||||
!receive_daemon_auth(file_descriptor, config) ||
|
||||
!receive_iconv_spec(file_descriptor, config) ||
|
||||
!receive_privilege_options(file_descriptor, config))
|
||||
goto error;
|
||||
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 &&
|
||||
strcmp(config->compress_choice, "none") != 0) {
|
||||
|
||||
+43
-2
@@ -402,6 +402,22 @@ typedef struct Config {
|
||||
IdentityMap* groupmap;
|
||||
int groupmap_count;
|
||||
|
||||
/* --super / --no-super (P7 Wave E, protocol 2.18.0): receiver-side privilege
|
||||
* policy for super-user activities confined below the authorized receive
|
||||
* root. SUPER_MODE_AUTO (default) preserves the pre-existing behavior: a
|
||||
* privileged operation is only attempted when the receiver is ALREADY root
|
||||
* (geteuid() == 0). SUPER_MODE_ON (--super) PERMITS the receiver to attempt
|
||||
* those activities (ownership application, char/block device-node creation)
|
||||
* even when it is not root -- the attempt is then confined exactly as before
|
||||
* and simply fails/skips if the kernel refuses it. SUPER_MODE_OFF
|
||||
* (--no-super) FORBIDS them even when running as root. FastSync NEVER
|
||||
* elevates privileges (no setuid/seteuid/setgid) and never bypasses the
|
||||
* fd-relative confinement (file_open_secure_parent, O_NOFOLLOW, root checks);
|
||||
* --super only permits an attempt that is already confined. Crosses the wire
|
||||
* as a trailing int so the receiver can enforce the policy. See
|
||||
* privilege_super_permitted() in identity.h. */
|
||||
int super_mode;
|
||||
|
||||
// Receiver-side runtime staging registry for --delay-updates. Never sent
|
||||
// over the wire and never set on the sender side.
|
||||
DelayUpdatesContext* delay_context;
|
||||
@@ -563,8 +579,24 @@ typedef struct Config {
|
||||
* would desynchronize on the unknown frame, and the strict same-version
|
||||
* handshake (config_receive rejects a mismatched version before parsing
|
||||
* anything else) is what keeps a 2.17 client and a 2.16 server from ever
|
||||
* reaching that state. */
|
||||
#define PROTOCOL_VERSION "2.17.0"
|
||||
* reaching that state.
|
||||
*
|
||||
* 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. */
|
||||
#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). */
|
||||
#define MAX_BASIS_DIRS 64
|
||||
@@ -577,6 +609,15 @@ 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. */
|
||||
#define SUPER_MODE_AUTO 0
|
||||
#define SUPER_MODE_ON 1
|
||||
#define SUPER_MODE_OFF 2
|
||||
|
||||
Config* config_create(void);
|
||||
void config_delete(Config* config);
|
||||
bool config_send(int file_descriptor, const Config* config);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "delay_updates.h"
|
||||
#include "delta.h"
|
||||
#include "file.h"
|
||||
#include "identity.h"
|
||||
#include "log.h"
|
||||
#include "metadata.h"
|
||||
#include "protocol.h"
|
||||
@@ -355,6 +356,17 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
if (is_char || is_blk) {
|
||||
if (!config || !config->preserve_devices)
|
||||
return FILE_SAVE_SKIPPED;
|
||||
/* --super / --no-super (P7 Wave E): char/block device-node creation is a
|
||||
super-user activity. --no-super forbids it even for a root receiver; the
|
||||
default AUTO only attempts it when already root. Pure FIFO creation is
|
||||
unprivileged and deliberately NOT gated here. */
|
||||
if (!privilege_super_permitted()) {
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"skipping %s: super-user device-node creation is not permitted "
|
||||
"(--no-super, or the receiver is not privileged)",
|
||||
file->path);
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
} else if (is_fifo) {
|
||||
if (!config || !config->preserve_specials)
|
||||
return FILE_SAVE_SKIPPED;
|
||||
|
||||
+48
-9
@@ -27,6 +27,10 @@ typedef struct {
|
||||
int usermap_count;
|
||||
IdentityMap* groupmap;
|
||||
int groupmap_count;
|
||||
/* --super / --no-super tri-state (SUPER_MODE_AUTO when unset). Snapshotted
|
||||
* per connection so privilege_super_permitted() can gate super-user
|
||||
* activities without a Config argument. */
|
||||
int super_mode;
|
||||
bool set;
|
||||
} IdentityActive;
|
||||
|
||||
@@ -44,6 +48,7 @@ static void identity_active_reset(void) {
|
||||
g_identity.chown_uid = 0;
|
||||
g_identity.chown_gid_set = false;
|
||||
g_identity.chown_gid = 0;
|
||||
g_identity.super_mode = SUPER_MODE_AUTO;
|
||||
g_identity.set = false;
|
||||
}
|
||||
|
||||
@@ -76,6 +81,7 @@ void identity_set_active(const Config* config) {
|
||||
g_identity.groupmap_count = config->groupmap_count;
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -86,6 +92,37 @@ void identity_set_active(const Config* config) {
|
||||
"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. */
|
||||
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");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* --super with NO explicit identity policy implies raw numeric-id preservation,
|
||||
* exactly as if --numeric-ids had been given. An explicit usermap/groupmap/
|
||||
* --chown/--numeric-ids always wins: identity_resolve_targets() checks those
|
||||
* before the numeric fallback, and this predicate is false whenever any of them
|
||||
* is present. In AUTO (the default) no implication is made, preserving the
|
||||
* opt-in-only behavior. */
|
||||
static bool identity_super_implies_numeric(void) {
|
||||
return g_identity.super_mode == SUPER_MODE_ON && !g_identity.numeric_ids &&
|
||||
!g_identity.chown_uid_set && !g_identity.chown_gid_set && g_identity.usermap_count == 0 &&
|
||||
g_identity.groupmap_count == 0;
|
||||
}
|
||||
|
||||
bool identity_active_enabled(void) {
|
||||
@@ -93,10 +130,11 @@ bool identity_active_enabled(void) {
|
||||
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. */
|
||||
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);
|
||||
stays inert; combined with -M it activates raw-id application. --super
|
||||
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());
|
||||
}
|
||||
|
||||
bool identity_wire_valid(const Config* config) {
|
||||
@@ -388,7 +426,7 @@ static bool identity_resolve_targets(const struct stat* st, int32_t source_uid,
|
||||
} else if (g_identity.chown_uid_set) {
|
||||
uid = g_identity.chown_uid == IDENTITY_CURRENT ? geteuid() : (uid_t)g_identity.chown_uid;
|
||||
set_uid = true;
|
||||
} else if (g_identity.numeric_ids) {
|
||||
} else if (g_identity.numeric_ids || identity_super_implies_numeric()) {
|
||||
uid = (uid_t)source_uid;
|
||||
set_uid = true;
|
||||
} else {
|
||||
@@ -412,7 +450,7 @@ static bool identity_resolve_targets(const struct stat* st, int32_t source_uid,
|
||||
} else if (g_identity.chown_gid_set) {
|
||||
gid = g_identity.chown_gid == IDENTITY_CURRENT ? getegid() : (gid_t)g_identity.chown_gid;
|
||||
set_gid = true;
|
||||
} else if (g_identity.numeric_ids) {
|
||||
} else if (g_identity.numeric_ids || identity_super_implies_numeric()) {
|
||||
gid = (gid_t)source_gid;
|
||||
set_gid = true;
|
||||
} else {
|
||||
@@ -458,8 +496,9 @@ static void identity_log_chown_failure(const char* what, uid_t uid, gid_t gid) {
|
||||
void 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. */
|
||||
if (!identity_active_enabled() || fd < 0)
|
||||
* 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)
|
||||
return;
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) != 0)
|
||||
@@ -474,7 +513,7 @@ void identity_apply_ownership(int fd, int32_t source_uid, int32_t source_gid) {
|
||||
|
||||
void identity_apply_ownership_link(int parent_fd, const char* leaf, int32_t source_uid,
|
||||
int32_t source_gid) {
|
||||
if (!identity_active_enabled() || parent_fd < 0 || !leaf)
|
||||
if (!identity_active_enabled() || !privilege_super_permitted() || parent_fd < 0 || !leaf)
|
||||
return;
|
||||
struct stat st;
|
||||
if (fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) != 0)
|
||||
|
||||
@@ -65,4 +65,14 @@ void identity_apply_ownership_link(int parent_fd, const char* leaf, int32_t sour
|
||||
/* Receiver-side wire validation of the resolved identity fields. */
|
||||
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. */
|
||||
bool privilege_super_permitted(void);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user