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:
@@ -170,5 +170,15 @@ bool validate_config(const Config* config) {
|
||||
PROTOCOL_VERSION);
|
||||
return false;
|
||||
}
|
||||
/* --copy-as pushes the source ids through the metadata path (it implies
|
||||
--preserve). A later --no-preserve would clear use_metadata, leaving the
|
||||
transfer with nothing to chown while the receiver gate would still pass.
|
||||
Refuse the combination up front rather than silently chowning nothing. */
|
||||
if (config->copy_as_set && !config->use_metadata) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--copy-as requires metadata preservation and cannot be combined with "
|
||||
"--no-preserve");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -190,6 +190,13 @@ void print_usage(void) {
|
||||
printf(" Names resolve on the source machine; @N for numerics.\n");
|
||||
printf(" (Metadata is enabled with --preserve; -M now means\n");
|
||||
printf(" rsync's --remote-option.)\n");
|
||||
printf(" --copy-as=USER[:GROUP] Force every written entry (files, dirs, symlinks\n");
|
||||
printf(" and special nodes) to USER[:GROUP], resolved on the\n");
|
||||
printf(" source machine like --chown. Requires a privileged\n");
|
||||
printf(" (root) receiver and implies --preserve; an\n");
|
||||
printf(" unprivileged receiver refuses the transfer. Never\n");
|
||||
printf(" switches process credentials (safe-subset; see\n");
|
||||
printf(" RSYNC_COMPAT.md). A daemon refuses it.\n");
|
||||
printf(" --chunk-size <n> Chunk size in bytes (default: %d)\n", DEFAULT_CHUNK_SIZE);
|
||||
printf(" --source-dir <path> Source directory\n");
|
||||
printf(" --dest-dir <path> Destination directory\n");
|
||||
|
||||
+28
-1
@@ -31,6 +31,10 @@ static int authorized_root_fd = -1;
|
||||
static bool allow_delete;
|
||||
static bool trust_sender;
|
||||
static bool allow_unauthenticated;
|
||||
/* --no-super operator veto: forces SUPER_MODE_OFF for every connection (even
|
||||
* root), so no super-user activity is attempted and any client --copy-as is
|
||||
* refused. Set once in main before the accept loop / stdio handler. */
|
||||
static bool server_no_super;
|
||||
static const char* required_client_cn;
|
||||
/* --iconv CONVERT_SPEC the server was itself started with (borrowed argv
|
||||
* pointer). Its LOCAL half may override the local charset the client assumed;
|
||||
@@ -181,7 +185,23 @@ static const char* server_module_gate(const Config* config, void* context) {
|
||||
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) {
|
||||
/* Daemon divergence (P7 Wave E): a daemon has no per-module opt-in for
|
||||
client-chosen ownership, so it refuses --copy-as outright even when running
|
||||
as root -- otherwise any anonymous client could pick an arbitrary owner.
|
||||
The standalone listener and the SSH-launched --stdio server keep honoring
|
||||
it (they serve exactly one operator-authorized root). */
|
||||
if (g_daemon_conf != NULL && config->copy_as_set) {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as is refused by the daemon (no per-module opt-in for "
|
||||
"client-chosen ownership); refusing");
|
||||
return "--copy-as is not permitted by this daemon";
|
||||
}
|
||||
/* Operator veto: --no-super forces SUPER_MODE_OFF for this connection before
|
||||
the copy-as gate is evaluated, and the caller clamps the accepted config
|
||||
again after this returns so the ownership/device gates see it too. */
|
||||
Config* effective = (Config*)config;
|
||||
if (server_no_super)
|
||||
effective->super_mode = SUPER_MODE_OFF;
|
||||
if (identity_copy_as_refused(effective)) {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as requires a privileged receiver (root); refusing");
|
||||
return "--copy-as requires a privileged receiver (root)";
|
||||
}
|
||||
@@ -283,6 +303,12 @@ void handler(int file_descriptor) {
|
||||
protocol_session_unbind();
|
||||
return;
|
||||
}
|
||||
/* Operator --no-super veto: clamp the accepted config so every downstream
|
||||
* gate (identity_apply_ownership via privilege_super_permitted, device-node
|
||||
* creation) sees SUPER_MODE_OFF even if the gate callback did not already
|
||||
* mutate a copy of it. */
|
||||
if (server_no_super)
|
||||
config->super_mode = SUPER_MODE_OFF;
|
||||
protocol_set_8_bit_output(config->eight_bit_output);
|
||||
if (!authorized_root) {
|
||||
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
||||
@@ -671,6 +697,7 @@ int main(int argc, char* argv[]) {
|
||||
allow_delete = opts.allow_delete;
|
||||
trust_sender = opts.trust_sender;
|
||||
allow_unauthenticated = opts.allow_unauthenticated;
|
||||
server_no_super = opts.no_super;
|
||||
server_iconv_spec = opts.iconv_spec;
|
||||
signal(SIGINT, cleanup);
|
||||
signal(SIGTERM, cleanup);
|
||||
|
||||
@@ -142,6 +142,8 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
|
||||
opts->allow_delete = true;
|
||||
} else if (arg_is(argv[i], "--trust-sender")) {
|
||||
opts->trust_sender = true;
|
||||
} else if (arg_is(argv[i], "--no-super")) {
|
||||
opts->no_super = true;
|
||||
} else if (arg_is(argv[i], "--allow-unauthenticated")) {
|
||||
opts->allow_unauthenticated = true;
|
||||
} else if (arg_is(argv[i], "--iconv")) {
|
||||
|
||||
@@ -33,6 +33,11 @@ typedef struct ServerCliOptions {
|
||||
bool allow_delete; /* --allow-delete */
|
||||
bool trust_sender; /* --trust-sender */
|
||||
bool allow_unauthenticated; /* --allow-unauthenticated */
|
||||
/* --no-super: operator veto forcing SUPER_MODE_OFF for every connection, so
|
||||
* the receiver never attempts super-user activities (ownership application,
|
||||
* device-node creation) even when running as root. Applies to --stdio and
|
||||
* --daemon alike; also makes the server refuse any client --copy-as. */
|
||||
bool no_super; /* --no-super */
|
||||
/* --iconv=CONVERT_SPEC: the server's own LOCAL charset declaration. The
|
||||
* client's full spec rides the wire config frame anyway; when the server is
|
||||
* started with its own --iconv, its LOCAL half overrides the local charset
|
||||
|
||||
@@ -246,6 +246,10 @@ static bool validate_received_config(const Config* config) {
|
||||
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)) &&
|
||||
/* --copy-as forces ownership through the metadata path; without
|
||||
metadata it would pass the privilege gate but silently chown
|
||||
nothing. Refuse the frame instead. */
|
||||
(!config->copy_as_set || config->use_metadata) &&
|
||||
(!config->use_compression ||
|
||||
(config->compression_level >= 1 && config->compression_level <= 22)) &&
|
||||
config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE &&
|
||||
|
||||
@@ -23,8 +23,13 @@
|
||||
* server's --destination-root: the daemon confines every connection that
|
||||
* selects this module to this path (file_open_secure_parent /
|
||||
* has_path_traversal / path_is_within all keep the existing confinement, just
|
||||
* per-module). There is never any client-chosen root and no --super /
|
||||
* --copy-as: a module path always stays confined.
|
||||
* per-module). There is never any client-chosen root: a module path always
|
||||
* stays confined. A daemon also REFUSES a client --copy-as outright, because
|
||||
* there is no per-module opt-in for client-chosen ownership (unlike the
|
||||
* standalone/SSH server, which honors it for its single operator-authorized
|
||||
* root); the operator-level --no-super veto additionally forces super-user
|
||||
* activities off for every daemon connection. See server_module_gate in
|
||||
* server.c and RSYNC_COMPAT.md.
|
||||
*
|
||||
* `auth_users` is honored by Wave B daemon authentication: a module that
|
||||
* declares auth users accepts a connection only when the presented username is
|
||||
|
||||
@@ -456,12 +456,19 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
return FILE_SAVE_SKIPPED;
|
||||
}
|
||||
|
||||
/* Apply mtime on the fresh node (utimensat, no-follow). Ownership is not
|
||||
applied -- identity fchown needs an fd and would require opening the node. */
|
||||
/* Apply mtime on the fresh node (utimensat, no-follow). */
|
||||
struct timespec times[2] = {
|
||||
{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
|
||||
{.tv_sec = file->metadata->mtime_sec, .tv_nsec = file->metadata->mtime_nsec}};
|
||||
utimensat(parent_fd, leaf, times, AT_SYMLINK_NOFOLLOW);
|
||||
/* P7 Wave E: apply the negotiated ownership to the node ITSELF. A FIFO is
|
||||
created unprivileged, but --copy-as and explicit identity policies own
|
||||
every entry (a char/block node path is already privilege-gated above). The
|
||||
no-follow helper changes the node's own ownership without dereferencing it;
|
||||
it is a no-op unless an identity policy is active. */
|
||||
if (identity_active_enabled())
|
||||
identity_apply_ownership_link(parent_fd, leaf, (int32_t)file->metadata->uid,
|
||||
(int32_t)file->metadata->gid);
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
free(destination);
|
||||
@@ -597,6 +604,22 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
if (!dir_path)
|
||||
return FILE_SAVE_ERROR;
|
||||
bool ok = file_ensure_directory_secure(dir_path);
|
||||
/* P7 Wave E: apply the negotiated ownership to the directory ITSELF (not
|
||||
just the files inside it). --copy-as and every explicit identity policy
|
||||
own every entry, so a directory must not keep the receiver's owner while
|
||||
its children get the policy owner. Applied no-follow on the confined
|
||||
parent fd after the mkdir; identity_apply_ownership_link() is itself a
|
||||
no-op unless an identity policy is active. */
|
||||
if (ok && file->metadata && identity_active_enabled()) {
|
||||
char* leaf = NULL;
|
||||
int parent_fd = file_open_secure_parent(dir_path, &leaf, false);
|
||||
if (parent_fd >= 0) {
|
||||
identity_apply_ownership_link(parent_fd, leaf, (int32_t)file->metadata->uid,
|
||||
(int32_t)file->metadata->gid);
|
||||
close(parent_fd);
|
||||
}
|
||||
free(leaf);
|
||||
}
|
||||
free(dir_path);
|
||||
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
+109
-18
@@ -148,10 +148,25 @@ bool identity_active_enabled(void) {
|
||||
metadata, never reaches identity_apply_ownership, and therefore correctly
|
||||
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 || g_identity.copy_as_set ||
|
||||
identity_super_implies_numeric());
|
||||
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 ||
|
||||
identity_super_implies_numeric());
|
||||
}
|
||||
|
||||
bool identity_copy_as_active(void) {
|
||||
return g_identity.set && g_identity.copy_as_set;
|
||||
}
|
||||
|
||||
bool identity_copy_as_refused(const Config* config) {
|
||||
if (!config || !config->copy_as_set)
|
||||
return false;
|
||||
/* The safe-subset --copy-as needs a privileged (root) receiver, and an
|
||||
* operator/--no-super veto forbids the ownership change even for root. This
|
||||
* is deliberately a pure function of the config and the current effective uid
|
||||
* (never the active snapshot) because the server evaluates it at the
|
||||
* pre-STATUS_OK config gate, before identity_set_active() has run. */
|
||||
return geteuid() != 0 || config->super_mode == SUPER_MODE_OFF;
|
||||
}
|
||||
|
||||
bool identity_wire_valid(const Config* config) {
|
||||
@@ -172,6 +187,12 @@ bool identity_wire_valid(const Config* config) {
|
||||
if (config->groupmap[i].from < IDENTITY_MATCH_ANY || config->groupmap[i].to < IDENTITY_CURRENT)
|
||||
return false;
|
||||
}
|
||||
/* Defense-in-depth: a --copy-as block must never carry a negative (sentinel)
|
||||
* id into the ownership path. receive_copy_as_options already rejects them,
|
||||
* but identity_wire_valid is the shared validation used by both the receiver
|
||||
* and unit tests, so re-assert it here. */
|
||||
if (config->copy_as_set && (config->copy_as_uid < 0 || config->copy_as_gid < 0))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -413,6 +434,13 @@ done:
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* uid_t/gid_t are unsigned and may hold a value wider than the signed int32 the
|
||||
* wire (and the identity policy) uses. Reject such an id instead of truncating
|
||||
* it to an out-of-range (possibly negative sentinel) value. */
|
||||
static bool identity_id_fits_int32(unsigned long id) {
|
||||
return id <= (unsigned long)INT32_MAX;
|
||||
}
|
||||
|
||||
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]");
|
||||
@@ -426,7 +454,10 @@ int identity_parse_copy_as(Config* config, const char* value) {
|
||||
if (*p == ':')
|
||||
colons++;
|
||||
if (colons > 1) {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as must be USER[:GROUP] (got '%s')", value);
|
||||
char* escaped = output_escape(value, false);
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as must be USER[:GROUP] (got '%s')",
|
||||
escaped ? escaped : "<allocation failed>");
|
||||
free(escaped);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -443,20 +474,34 @@ int identity_parse_copy_as(Config* config, const char* value) {
|
||||
group_token = colon + 1;
|
||||
}
|
||||
|
||||
/* The spec is untrusted user input echoed back in error paths: escape it once
|
||||
* (8-bit-safe) so a control byte cannot forge a log line. */
|
||||
char* escaped_spec = output_escape(value, false);
|
||||
const char* shown = escaped_spec ? escaped_spec : "<allocation failed>";
|
||||
|
||||
int32_t uid;
|
||||
if (*user_token == '\0') {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as is missing the user (got '%s')", value);
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as is missing the user (got '%s')", shown);
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
if (strcmp(user_token, "*") == 0) {
|
||||
/* '*' means the current/root user: the client's euid. */
|
||||
if (!identity_id_fits_int32((unsigned long)geteuid())) {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as: current user id %lu exceeds INT32_MAX",
|
||||
(unsigned long)geteuid());
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
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);
|
||||
"--copy-as could not resolve user (use a name that exists on the "
|
||||
"source, '*', or @N): %s",
|
||||
shown);
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
@@ -464,15 +509,24 @@ int identity_parse_copy_as(Config* config, const char* value) {
|
||||
int32_t gid;
|
||||
if (group_token) {
|
||||
if (*group_token == '\0') {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as group is empty (got '%s')", value);
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as group is empty (got '%s')", shown);
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
if (strcmp(group_token, "*") == 0) {
|
||||
if (!identity_id_fits_int32((unsigned long)getegid())) {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as: current group id %lu exceeds INT32_MAX",
|
||||
(unsigned long)getegid());
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
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);
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as could not resolve group (got '%s'): %s", shown,
|
||||
shown);
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
@@ -481,8 +535,30 @@ int identity_parse_copy_as(Config* config, const char* value) {
|
||||
* 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;
|
||||
if (pw) {
|
||||
if (!identity_id_fits_int32((unsigned long)pw->pw_gid)) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--copy-as: primary group id %lu for the requested user exceeds INT32_MAX",
|
||||
(unsigned long)pw->pw_gid);
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
gid = (int32_t)pw->pw_gid;
|
||||
} else {
|
||||
gid = uid;
|
||||
}
|
||||
}
|
||||
/* The group-default and gid==uid fallbacks must never store a negative
|
||||
* (sentinel) value; the explicit numeric path is already capped by
|
||||
* identity_resolve_token. */
|
||||
if (uid < 0 || gid < 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "--copy-as resolved id does not fit in int32 (got '%s')", shown);
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
return -1;
|
||||
}
|
||||
free(escaped_spec);
|
||||
free(spec);
|
||||
|
||||
config->copy_as_set = true;
|
||||
@@ -596,13 +672,28 @@ 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
|
||||
* error (EIO/EROFS/ENOSPC/...) is a real failure and must not be silently
|
||||
* downgraded to a warning. */
|
||||
if (errno == EPERM || errno == EACCES)
|
||||
log_message(LOG_LEVEL_WARNING, "could not apply ownership (uid=%ld gid=%ld): %s; leaving as-is",
|
||||
(long)uid, (long)gid, strerror(errno));
|
||||
else
|
||||
* downgraded to a warning.
|
||||
*
|
||||
* --copy-as is different: the whole point of the flag is that the target
|
||||
* ownership is REQUIRED (the pre-flight gate already refused an unprivileged
|
||||
* receiver). If the chown still fails with EPERM/EACCES (a capability-
|
||||
* restricted root, root-squash, or a read-only mount) the run is silently
|
||||
* producing the WRONG ownership, so surface it at ERROR. It stays
|
||||
* non-fatal: never abort the multithreaded receiver mid-transfer. */
|
||||
if (errno == EPERM || errno == EACCES) {
|
||||
if (identity_copy_as_active())
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"could not apply --copy-as ownership on %s (uid=%ld gid=%ld): %s; "
|
||||
"entry was written with the wrong owner",
|
||||
what, (long)uid, (long)gid, strerror(errno));
|
||||
else
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"could not apply ownership (uid=%ld gid=%ld): %s; leaving as-is", (long)uid,
|
||||
(long)gid, strerror(errno));
|
||||
} else {
|
||||
log_message(LOG_LEVEL_ERROR, "failed to apply ownership on %s (uid=%ld gid=%ld): %s", what,
|
||||
(long)uid, (long)gid, strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
void identity_apply_ownership(int fd, int32_t source_uid, int32_t source_gid) {
|
||||
|
||||
@@ -56,6 +56,14 @@ int identity_parse_copy_as(Config* config, const char* value);
|
||||
* server-side policy veto. */
|
||||
bool identity_copy_as_refused(const Config* config);
|
||||
|
||||
/* True when the CURRENT per-connection snapshot has a --copy-as active (i.e.
|
||||
* identity_set_active() has run against a config with copy_as_set). The
|
||||
* --fake-super owner replay consults this so a copy-as run never lets the
|
||||
* recorded source owner overwrite the forced target owner. Reads the active
|
||||
* snapshot, so call identity_set_active() first (the receiver does, before any
|
||||
* write). */
|
||||
bool identity_copy_as_active(void);
|
||||
|
||||
/* 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
|
||||
|
||||
+16
-3
@@ -1,5 +1,6 @@
|
||||
#define _GNU_SOURCE
|
||||
#include "xattr.h"
|
||||
#include "identity.h"
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include "utils.h"
|
||||
@@ -337,7 +338,16 @@ void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, int6
|
||||
* stat fd-relative. A privileged (root) run can actually change the owner;
|
||||
* a non-root run silently skips the fchown on EPERM/EACCES (never fatal,
|
||||
* mirroring the normal metadata identity path; other errors are logged) and
|
||||
* still applies mode/mtime where permitted. */
|
||||
* still applies mode/mtime where permitted.
|
||||
*
|
||||
* The OWNER leg additionally honors two policies:
|
||||
* - --no-super (privilege_super_permitted() false) suppresses it even for a
|
||||
* root receiver, exactly like the normal metadata identity path.
|
||||
* - an active --copy-as is AUTHORITATIVE: the identity path already forced the
|
||||
* target owner, so replaying the recorded source owner here would silently
|
||||
* override it. The xattr record is still stored/replayed for a later
|
||||
* privileged restore; only the live chown is skipped. Mode/mtime remain
|
||||
* applied either way so unprivileged --fake-super still works. */
|
||||
bool fake_super_restore_fd(int fd) {
|
||||
if (fd < 0)
|
||||
return false;
|
||||
@@ -357,8 +367,11 @@ bool fake_super_restore_fd(int fd) {
|
||||
must not abort the transfer for that reason (FastSync identity philosophy).
|
||||
EPERM/EACCES (expected for a non-root receiver) are skipped silently; a
|
||||
genuine EINVAL (an impossible stored id) is logged so the corruption is
|
||||
not hidden. */
|
||||
if (fchown(fd, (uid_t)ul_uid, (gid_t)ul_gid) != 0 && errno != EPERM && errno != EACCES)
|
||||
not hidden. --no-super suppresses the owner leg even for root, and an
|
||||
active --copy-as is authoritative so its forced owner must not be
|
||||
overwritten by the recorded source owner. */
|
||||
if (privilege_super_permitted() && !identity_copy_as_active() &&
|
||||
fchown(fd, (uid_t)ul_uid, (gid_t)ul_gid) != 0 && errno != EPERM && errno != EACCES)
|
||||
log_message(LOG_LEVEL_WARNING, "--fake-super: could not restore owner on destination file: %s",
|
||||
strerror(errno));
|
||||
/* Mode is applied through the same sanitization the normal metadata path
|
||||
|
||||
Reference in New Issue
Block a user