feat: per-attribute preservation flags -p/-t/-o/-g with --no-* negations (protocol 2.22.0)
Split FastSync's single use_metadata bundle into four independent rsync-parity attributes: preserve_perms, preserve_times, preserve_owner, preserve_group. use_metadata is now a derived transport bit (config_derived_use_metadata). CLI: real -p/--perms, -t/--times, -o/--owner, -g/--group plus --no-perms/--no-times/--no-owner/--no-group (short and long) and --no-preserve; -a is now rsync -rlptgoD; --preserve = -pt; -A implies -p; -X does not; --chmod implies -p; --usermap/--groupmap/--chown imply owner/group per side; --incremental/--delta still auto-preserve unless negated. Receiver: per-attribute FileAttrPolicy gating for files, dirs (modes applied at end of transfer), symlinks and specials; rsync -E read-bit rule; new files get source_mode & ~umask sanitized (no group/other write); per-side identity resolution; deferred directory metadata; batch dir-metadata replay; daemon modules without 'client owner = yes' no longer refuse plain -a but force super off (no ownership) with a warning. Wire: PROTOCOL_VERSION 2.21.0 -> 2.22.0 (four appended config bools, golden 653 / 95530566005420798). FileMetadata/chunk/batch framing unchanged. Docs/CHANGELOG/CMake updated to 2.22.0.
This commit is contained in:
+104
-54
@@ -619,6 +619,11 @@ typedef struct {
|
||||
size_t offset; /* offsetof of the boolean target field in Config */
|
||||
} NegatableOption;
|
||||
|
||||
/* Sentinel offset for --no-preserve, the rsync drop-in negation of the whole
|
||||
* preservation bundle: it clears all four per-attribute flags and records the
|
||||
* explicit metadata opt-out instead of clearing a single Config field. */
|
||||
#define NEGATABLE_PRESERVE_BUNDLE ((size_t) - 1)
|
||||
|
||||
/* Options that map directly onto a Config field with no side effects.
|
||||
*
|
||||
* NOTE: these CLI tables are intentionally NOT generated from the wire-field
|
||||
@@ -796,7 +801,11 @@ static const NegatableOption NEGATABLE_OPTIONS[] = {
|
||||
{"compress", NULL, offsetof(Config, use_compression)},
|
||||
{"compress", "z", offsetof(Config, use_compression)},
|
||||
{"multithreading", "j", offsetof(Config, use_multithreading)},
|
||||
{"preserve", NULL, offsetof(Config, use_metadata)},
|
||||
{"preserve", NULL, NEGATABLE_PRESERVE_BUNDLE},
|
||||
{"perms", "p", offsetof(Config, preserve_perms)},
|
||||
{"times", "t", offsetof(Config, preserve_times)},
|
||||
{"owner", "o", offsetof(Config, preserve_owner)},
|
||||
{"group", "g", offsetof(Config, preserve_group)},
|
||||
{"sendfile", NULL, offsetof(Config, use_sendfile)},
|
||||
{"chunk-serialization", NULL, offsetof(Config, use_chunk_serialization)},
|
||||
{"xattrs", "X", offsetof(Config, preserve_xattrs)},
|
||||
@@ -856,9 +865,27 @@ static int apply_negation(Config* config, const char* arg) {
|
||||
fprintf(stderr, "Cannot negate unsupported or unsafe option: %s\n", arg);
|
||||
return -1;
|
||||
}
|
||||
*(bool*)((char*)config + entry->offset) = false;
|
||||
if (entry->offset == offsetof(Config, use_metadata))
|
||||
if (entry->offset == NEGATABLE_PRESERVE_BUNDLE) {
|
||||
config->preserve_perms = false;
|
||||
config->preserve_times = false;
|
||||
config->preserve_owner = false;
|
||||
config->preserve_group = false;
|
||||
config->metadata_explicitly_disabled = true;
|
||||
/* --no-preserve is an explicit opt-out of the whole bundle: record it so
|
||||
* the --incremental/--delta auto-preserve in cli_finalize_config does not
|
||||
* silently re-enable perms/times. */
|
||||
config->preserve_perms_explicit_off = true;
|
||||
config->preserve_times_explicit_off = true;
|
||||
return 0;
|
||||
}
|
||||
*(bool*)((char*)config + entry->offset) = false;
|
||||
/* Track an explicit per-attribute negation so --incremental/--delta can
|
||||
* auto-preserve the OTHER attribute without undoing this one. A later
|
||||
* -p/-t sets the attribute directly; this flag only gates the implication. */
|
||||
if (entry->offset == offsetof(Config, preserve_perms))
|
||||
config->preserve_perms_explicit_off = true;
|
||||
else if (entry->offset == offsetof(Config, preserve_times))
|
||||
config->preserve_times_explicit_off = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -869,8 +896,6 @@ static int apply_table_option(Config* config, const OptionEntry* entry, const ch
|
||||
switch (entry->kind) {
|
||||
case OPT_FLAG:
|
||||
*(bool*)field = true;
|
||||
if (entry->offset == offsetof(Config, update))
|
||||
config->use_metadata = true;
|
||||
return 0;
|
||||
case OPT_NOOP:
|
||||
return 0;
|
||||
@@ -1115,7 +1140,7 @@ static bool cli_handle_table_option(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
config->preserve_perms = true;
|
||||
}
|
||||
/* Remember that --server-host was explicitly given (the field itself
|
||||
defaults to 127.0.0.1, so a value check cannot distinguish it). Used
|
||||
@@ -1142,21 +1167,15 @@ static bool cli_handle_table_option(CliParseCtx* ctx) {
|
||||
config. */
|
||||
if (entry->offset == offsetof(Config, delete_missing_args))
|
||||
config->ignore_missing_args = true;
|
||||
/* -U/--atimes and -N/--crtimes carry their times inside the metadata
|
||||
payload, which is only transmitted when use_metadata is set, so either
|
||||
one implies metadata transmission. This is FastSync's broad -M bundle
|
||||
(mode/mtime travel too); it does NOT enable ownership application,
|
||||
which stays opt-in via the identity flags. */
|
||||
if (entry->offset == offsetof(Config, preserve_atimes) ||
|
||||
entry->offset == offsetof(Config, preserve_crtimes))
|
||||
config->use_metadata = true;
|
||||
/* -A/--acls implies permission preservation as well as the xattr channel;
|
||||
-X/--xattrs preserves only the extended attributes. Either sets the
|
||||
derived xattr transport bit so the sender emits the per-file xattr block. */
|
||||
if (entry->offset == offsetof(Config, preserve_xattrs) ||
|
||||
entry->offset == offsetof(Config, preserve_acls)) {
|
||||
config->use_metadata = true;
|
||||
config->use_xattrs = config->preserve_acls || config->preserve_xattrs;
|
||||
if (entry->offset == offsetof(Config, preserve_acls))
|
||||
config->preserve_perms = true;
|
||||
}
|
||||
if (entry->offset == offsetof(Config, fake_super))
|
||||
config->use_metadata = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1178,7 +1197,7 @@ static bool cli_handle_inline_chmod(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
config->preserve_perms = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1206,29 +1225,44 @@ static bool cli_handle_meta_flags(CliParseCtx* ctx) {
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "-a", "--archive")) {
|
||||
/* FastSync archive mode (-rlptD). FastSync is always recursive and always
|
||||
* preserves hard-link/other transfer semantics per its own flags, so -a
|
||||
* implies links, metadata (perms/times), devices and specials. Owner/group
|
||||
* are NOT implied; they require an explicit identity flag
|
||||
* (--numeric-ids/--usermap/--groupmap/--chown/--copy-as). Compression and
|
||||
* multithreading are NOT implied either (they are no longer part of
|
||||
* archive mode). */
|
||||
/* FastSync archive mode (-rlptgoD). FastSync is always recursive and
|
||||
* always preserves hard-link/other transfer semantics per its own flags, so
|
||||
* -a implies links plus the four per-attribute preservation flags
|
||||
* (perms/times/owner/group), devices and specials. Compression and
|
||||
* multithreading are NOT implied (they are no longer part of archive
|
||||
* mode). */
|
||||
config->follow_symlinks = true;
|
||||
config->use_metadata = true;
|
||||
config->preserve_perms = true;
|
||||
config->preserve_times = true;
|
||||
config->preserve_owner = true;
|
||||
config->preserve_group = true;
|
||||
config->preserve_devices = true;
|
||||
config->preserve_specials = true;
|
||||
log_info_message(LOG_INFO_MISC,
|
||||
"Enabled archive mode (-rlptD: links, metadata, devices, specials; "
|
||||
"owner/group opt-in)");
|
||||
"Enabled archive mode (-rlptgoD: links, perms, times, owner, group, "
|
||||
"devices, specials)");
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "-p", "--perms")) {
|
||||
/* rsync -p/--perms: preserve permission bits. Folded into FastSync's
|
||||
* broad metadata bundle (mode/mtime travel together). */
|
||||
config->use_metadata = true;
|
||||
config->preserve_perms = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled permission preservation");
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "-t", "--times")) {
|
||||
config->preserve_times = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled time preservation");
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "-o", "--owner")) {
|
||||
config->preserve_owner = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled owner preservation");
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "-g", "--group")) {
|
||||
config->preserve_group = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled group preservation");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1357,12 +1391,12 @@ static bool cli_handle_transfer_flags(CliParseCtx* ctx) {
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "--preserve", NULL)) {
|
||||
config->use_metadata = true;
|
||||
config->preserve_perms = true;
|
||||
config->preserve_times = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled metadata preservation");
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "-E", "--executability")) {
|
||||
config->use_metadata = true;
|
||||
config->use_executability = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled executable permission preservation");
|
||||
return true;
|
||||
@@ -1806,7 +1840,7 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
config->preserve_owner = true;
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "--usermap", NULL)) {
|
||||
@@ -1819,7 +1853,7 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
config->preserve_owner = true;
|
||||
return true;
|
||||
}
|
||||
if (strncmp(arg, "--groupmap=", 11) == 0) {
|
||||
@@ -1827,7 +1861,7 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
config->preserve_group = true;
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "--groupmap", NULL)) {
|
||||
@@ -1840,7 +1874,7 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
config->preserve_group = true;
|
||||
return true;
|
||||
}
|
||||
if (strncmp(arg, "--chown=", 8) == 0) {
|
||||
@@ -1848,7 +1882,10 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
if (config->chown_uid_set)
|
||||
config->preserve_owner = true;
|
||||
if (config->chown_gid_set)
|
||||
config->preserve_group = true;
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "--chown", NULL)) {
|
||||
@@ -1861,7 +1898,10 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
if (config->chown_uid_set)
|
||||
config->preserve_owner = true;
|
||||
if (config->chown_gid_set)
|
||||
config->preserve_group = true;
|
||||
return true;
|
||||
}
|
||||
if (strncmp(arg, "--copy-as=", 10) == 0) {
|
||||
@@ -1921,19 +1961,10 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
|
||||
config->files_from_set = set;
|
||||
}
|
||||
|
||||
/* Device/special preservation recreates a node from its metadata mode (whose
|
||||
S_IFMT bits carry the node kind), so --devices/--specials/-D imply metadata
|
||||
transmission. --copy-devices/--write-devices treat the entry as data but a
|
||||
mtime/mode-preserving transfer still benefits from metadata, so all four
|
||||
imply it (FastSync's broad -M bundle; ownership stays opt-in). */
|
||||
if (config->preserve_devices || config->preserve_specials || config->copy_devices ||
|
||||
config->write_devices)
|
||||
config->use_metadata = true;
|
||||
|
||||
/* The "unchanged" decision for --compare-dest/--copy-dest/--link-dest must
|
||||
* be made on the receiver against the basis directories, which requires the
|
||||
* per-file STATUS_CHECK handshake: basis-dir options therefore imply
|
||||
* --incremental (and, via the block below, metadata) on the sender. */
|
||||
* --incremental (and, via the derived bit below, metadata) on the sender. */
|
||||
if (config_has_basis(config))
|
||||
config->use_incremental = true;
|
||||
|
||||
@@ -1966,12 +1997,27 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
|
||||
config->use_incremental = true;
|
||||
}
|
||||
|
||||
/* Incremental and delta transfers need metadata unless the user disabled it. */
|
||||
if ((config->use_incremental || config->use_delta) && !config->use_metadata &&
|
||||
!config->metadata_explicitly_disabled) {
|
||||
log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for incremental/delta transfer");
|
||||
config->use_metadata = true;
|
||||
/* --incremental/--delta historically auto-enabled the metadata path, which
|
||||
* applied mode+mtime (README: "--incremental Auto-enables --preserve").
|
||||
* Restore that behavior by turning on the two attributes unless the user
|
||||
* explicitly negated them (--no-perms/--no-times/--no-preserve). This runs
|
||||
* BEFORE the derived use_metadata bit so the transport frame is still sent
|
||||
* for the incremental/delta handshake even when both attributes were negated
|
||||
* via --no-preserve (metadata_explicitly_disabled handles that opt-out). */
|
||||
if ((config->use_incremental || config->use_delta) && !config->metadata_explicitly_disabled) {
|
||||
if (!config->preserve_perms_explicit_off)
|
||||
config->preserve_perms = true;
|
||||
if (!config->preserve_times_explicit_off)
|
||||
config->preserve_times = true;
|
||||
}
|
||||
|
||||
/* Derive the transport bit from the FINAL parsed flags. Every
|
||||
* preservation/ownership option that needs the metadata frame (per-attribute
|
||||
* perms/times/owner/group, atimes/crtimes, executability, xattrs/acls,
|
||||
* fake-super, devices/specials, chmod, identity maps/chown/copy-as, and the
|
||||
* incremental/delta handshake unless --no-preserve explicitly disabled it) is
|
||||
* centralized in config_derived_use_metadata(). */
|
||||
config->use_metadata = config_derived_use_metadata(config);
|
||||
/* Recompute the derived xattr flag from the FINAL preserve flags (after any
|
||||
* --no-xattrs/--no-acls negation) so the sender's wire gate always matches
|
||||
* the flags the receiver will recompute from the received config. */
|
||||
@@ -2121,6 +2167,10 @@ static int load_daemon_credentials(Config* config) {
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
/* Capture the process umask now, while still single-threaded: the cached
|
||||
* value is what file_mode_base() uses, and reading it later would race with
|
||||
* receiver threads creating files. */
|
||||
file_umask_capture();
|
||||
/* The server may close a connection mid-stream (e.g. when it rejects an
|
||||
oversized delta). Ignore SIGPIPE so that a broken TCP connection
|
||||
surfaces as a clean write error instead of killing the client. */
|
||||
|
||||
@@ -176,9 +176,10 @@ static bool prepare_scanner(const Config* config, int num_threads, PreparedScann
|
||||
options->excluded_paths = NULL;
|
||||
options->excluded_mutex = NULL;
|
||||
options->hardlinks = NULL;
|
||||
/* P7 Wave D: capture source directory times whenever metadata rides the
|
||||
wire. Whether they are APPLIED is decided receiver-side (-O skips). */
|
||||
options->capture_dir_times = config->use_metadata;
|
||||
/* P7 Wave D: capture source directory metadata when a directory attribute is
|
||||
requested (-p for modes, -t for times unless -O omits them). Whether they
|
||||
are APPLIED is decided receiver-side. */
|
||||
options->capture_dir_times = dir_metadata_should_capture(config);
|
||||
options->dir_entries = NULL;
|
||||
options->dir_entries_mutex = NULL;
|
||||
if (config->preserve_hard_links) {
|
||||
@@ -1444,7 +1445,8 @@ static bool send_directory_entry(const Client* client, File* file, const Config*
|
||||
stays within the receiver's bound, and a frame that would exceed it is never
|
||||
emitted. */
|
||||
static bool send_dir_times(const Client* client, const Config* config, ArrayList* dir_entries) {
|
||||
if (!client || !config || !config->use_metadata || !dir_entries || dir_entries->size == 0)
|
||||
if (!client || !config || !dir_metadata_should_capture(config) || !dir_entries ||
|
||||
dir_entries->size == 0)
|
||||
return true;
|
||||
int fd = client->file_descriptor;
|
||||
int index = 0;
|
||||
@@ -2250,7 +2252,7 @@ int send_files(Config* config) {
|
||||
receive_daemon_motd(client, config);
|
||||
if (!prepare_scanner(config, 0, &prepared))
|
||||
goto send_fail;
|
||||
if (config->use_metadata) {
|
||||
if (dir_metadata_should_capture(config)) {
|
||||
dir_entries = array_list_create(file_destroy);
|
||||
if (!dir_entries)
|
||||
goto send_fail;
|
||||
|
||||
+19
-10
@@ -20,12 +20,15 @@ void print_usage(void) {
|
||||
printf("Options:\n");
|
||||
printf(" -c, --checksum Verify content by checksum instead of size+mtime\n");
|
||||
printf(" -z, --compress [level] Enable compression (level 1-22, default 5)\n");
|
||||
printf(" -a, --archive rsync archive mode (-rlptD): links, perms, times,\n");
|
||||
printf(" devices and specials; owner/group are not implied;\n");
|
||||
printf(" not compression/multithreading\n");
|
||||
printf(" -a, --archive rsync archive mode (-rlptgoD): links, perms, times,\n");
|
||||
printf(" owner, group, devices and specials; not\n");
|
||||
printf(" compression/multithreading\n");
|
||||
printf(" -n, --dry-run Show what would be transferred\n");
|
||||
printf(" --remove-source-files Remove regular source files after successful transfer\n");
|
||||
printf(" -p, --perms Preserve permission bits (part of the metadata bundle)\n");
|
||||
printf(" -p, --perms Preserve permission bits\n");
|
||||
printf(" -t, --times Preserve modification times\n");
|
||||
printf(" -o, --owner Preserve owner (uid)\n");
|
||||
printf(" -g, --group Preserve group (gid)\n");
|
||||
printf(" --ssh-port <port> SSH port (default: 22)\n");
|
||||
printf(" -e, --rsh <command> Remote shell to launch on the client for the SSH\n");
|
||||
printf(" transport (default: ssh). The command may include\n");
|
||||
@@ -161,7 +164,12 @@ void print_usage(void) {
|
||||
printf(" --debug=FLAGS Fine-grained debug logging (use --debug=help for flags)\n");
|
||||
printf(" --info=FLAGS Fine-grained info: copy,misc,skip,stats,all,none\n");
|
||||
printf(" none suppresses info even with --verbose\n");
|
||||
printf(" --preserve Preserve file metadata (long form only)\n");
|
||||
printf(" --preserve Preserve permissions and times (= -pt; long form only)\n");
|
||||
printf(" --no-perms Negate -p/--perms\n");
|
||||
printf(" --no-times Negate -t/--times\n");
|
||||
printf(" --no-owner Negate -o/--owner\n");
|
||||
printf(" --no-group Negate -g/--group\n");
|
||||
printf(" --no-preserve Disable metadata preservation (negates --preserve)\n");
|
||||
printf(" -E, --executability Preserve executable permission bits\n");
|
||||
printf(" -U, --atimes Preserve access times\n");
|
||||
printf(" -N, --crtimes Capture birth time; cannot be applied (documented\n");
|
||||
@@ -180,8 +188,9 @@ void print_usage(void) {
|
||||
printf(" (char/block device-node creation, --write-devices)\n");
|
||||
printf(" within the confined receive root. Never elevates\n");
|
||||
printf(" privileges and never bypasses confinement; ownership\n");
|
||||
printf(" is still applied only with an explicit identity flag\n");
|
||||
printf(" (--numeric-ids/--chown/--usermap/--groupmap/--copy-as)\n");
|
||||
printf(" is still applied only with -o/--owner, -g/--group, or an\n");
|
||||
printf(" explicit identity flag (--numeric-ids/--chown/--usermap/\n");
|
||||
printf(" --groupmap/--copy-as)\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");
|
||||
@@ -196,12 +205,12 @@ void print_usage(void) {
|
||||
printf(" USER:GROUP, USER (owner only), :GROUP (group only); a\n");
|
||||
printf(" value of * means the current/root user as appropriate.\n");
|
||||
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(" (Implies owner/group metadata; -M now means rsync's\n");
|
||||
printf(" --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(" (root) receiver and implies owner/group metadata; 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");
|
||||
|
||||
@@ -470,11 +470,12 @@ static bool receiver_save_file(File* file, void* context_pointer) {
|
||||
} else {
|
||||
result = file_save_to_disk_full(context->config->receive_root_directory, file, context->config);
|
||||
}
|
||||
/* A directory's times are deferred, never applied inline: collect the
|
||||
metadata now and apply it at the end. -O/--omit-dir-times is honored by
|
||||
dir_time_list_apply's caller (see receiver_send_success_frame). */
|
||||
/* A directory's metadata is deferred, never applied inline: collect it now
|
||||
and apply it at the end. -O/--omit-dir-times and --preserve_perms/-times
|
||||
are honored by dir_metadata_list_apply's caller (see
|
||||
receiver_send_success_frame). */
|
||||
if (result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
||||
dir_times_should_capture(context->config) &&
|
||||
dir_metadata_should_capture(context->config) &&
|
||||
!dir_time_list_add(&context->dir_times, file->path, file->metadata)) {
|
||||
file_destroy(file);
|
||||
return false;
|
||||
@@ -514,7 +515,8 @@ static bool receiver_send_success_frame(int fd, void* context_pointer) {
|
||||
phases have committed, so it is finally safe to stamp directory times.
|
||||
This runs after the deferred deletion because receiver_process commits it
|
||||
before calling this success frame. */
|
||||
dir_time_list_apply(&context->dir_times, context->config->receive_root_directory);
|
||||
dir_metadata_list_apply(&context->dir_times, context->config->receive_root_directory,
|
||||
context->config);
|
||||
return receiver_send_final_success(fd, context->config, &context->outcomes);
|
||||
}
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ int write_thread(void* pipeline_context) {
|
||||
write would clobber them); accumulate the metadata here and let the
|
||||
caller apply it once every writer has drained. */
|
||||
if (!dry_run && result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
||||
dir_times_should_capture(context->config) &&
|
||||
dir_metadata_should_capture(context->config) &&
|
||||
!dir_time_list_add(&context->dir_times, file->path, file->metadata)) {
|
||||
file_destroy(file);
|
||||
pipeline_context_receiver_note_bytes_released(context, file_bytes);
|
||||
|
||||
+23
-3
@@ -389,8 +389,12 @@ static const char* module_gate_check_ownership(const Config* config, const Daemo
|
||||
ModuleGateContext* gate_ctx) {
|
||||
if (module->client_owner)
|
||||
return NULL;
|
||||
/* Ownership: refuse the whole transfer up front (a clear failure). */
|
||||
if (identity_ownership_requested(config)) {
|
||||
/* Ownership: refuse the whole transfer up front (a clear failure) for a
|
||||
* client-CHOSEN owner/group request. A plain -o/-g/-a preserve-source
|
||||
* request is deliberately not in this narrow set: it falls through to the
|
||||
* super-mode override below, which forces all ownership activity off for this
|
||||
* connection so no chown happens (the transfer itself still succeeds). */
|
||||
if (identity_explicit_ownership_requested(config)) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"daemon module '%s' refuses client-chosen ownership/super-user activities "
|
||||
"(no `client owner = yes` opt-in); refusing",
|
||||
@@ -737,6 +741,14 @@ void handler(int file_descriptor) {
|
||||
* received config. */
|
||||
if (gate_ctx.super_mode_override != -1)
|
||||
config->super_mode = (SuperMode)gate_ctx.super_mode_override;
|
||||
/* If the client requested ownership but the effective super mode forbids it
|
||||
* (operator --no-super, a privileged standalone receiver's secure default, or
|
||||
* a daemon module without `client owner = yes`), say so ONCE per connection so
|
||||
* a successful -a/-o/-g transfer is not mistaken for preserved ownership. */
|
||||
if (config->super_mode == SUPER_MODE_OFF && identity_ownership_requested(config))
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"requested ownership will NOT be applied: super-user activities are disabled "
|
||||
"for this connection (operator veto, or module without `client owner = yes`)");
|
||||
protocol_set_8_bit_output(config->eight_bit_output);
|
||||
/* Server-side per-message protocol deadline for every frame from here on.
|
||||
* `timeout` is not serialized, so this is the server's own config (the server
|
||||
@@ -959,7 +971,7 @@ void handler(int file_descriptor) {
|
||||
to stamp directory times; a directory's mtime must not be clobbered by
|
||||
its children or by an extra removal. */
|
||||
if (transfer_ok)
|
||||
dir_time_list_apply(&context->dir_times, config->receive_root_directory);
|
||||
dir_metadata_list_apply(&context->dir_times, config->receive_root_directory, config);
|
||||
}
|
||||
if (transfer_ok) {
|
||||
if (!receiver_send_final_success(file_descriptor, config, &context->outcomes))
|
||||
@@ -1127,10 +1139,18 @@ static bool daemonize(void) {
|
||||
if (chdir("/") != 0)
|
||||
log_message(LOG_LEVEL_WARNING, "daemon: chdir to / failed: %s", strerror(errno));
|
||||
umask(0);
|
||||
/* Refresh the cached umask: main() captured the launch umask before this
|
||||
* (single-threaded) umask(0), and file_mode_base() must see the daemon's
|
||||
* actual umask. */
|
||||
file_umask_capture();
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
/* Capture the process umask now, while still single-threaded: the cached
|
||||
* value is what file_mode_base() uses, and reading it later would race with
|
||||
* receiver threads creating files. */
|
||||
file_umask_capture();
|
||||
ServerCliOptions opts;
|
||||
char cli_err[512];
|
||||
int parse_result = server_cli_parse(argc, argv, &opts, cli_err, sizeof(cli_err));
|
||||
|
||||
+55
-20
@@ -2,6 +2,7 @@
|
||||
#include "data.h"
|
||||
#include "file.h"
|
||||
#include "file_receive.h"
|
||||
#include "identity.h"
|
||||
#include "log.h"
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
@@ -10,11 +11,15 @@
|
||||
|
||||
/* Serialization metadata mode for the batch stream, captured from the config at
|
||||
* batch_write_header time. The header persists it into the file so a batch is
|
||||
* self-describing: batch_read_apply re-reads it from the file (not from the
|
||||
* reading config), so a batch written with -M is applied identically by an
|
||||
* invoking process regardless of its own -M setting. The batch driver is a
|
||||
* single sequential scan pass within one thread, so this module-level flag is
|
||||
* safe. */
|
||||
* self-describing about whether per-entry metadata was CAPTURED in the stream:
|
||||
* batch_read_apply re-reads it from the file (not from the reading config) to
|
||||
* decode the chunk records correctly. Which attributes are actually APPLIED,
|
||||
* however, comes from the INVOKING process's per-attribute config (the
|
||||
* FileAttrPolicy and the dir-metadata gate), so a batch written with -M is NOT
|
||||
* automatically applied identically by an invoking process with a different
|
||||
* -p/-t/-o/-g: --read-batch must be invoked with the same -p/-t/-o/-g as the
|
||||
* write side (rsync requires the same options). The batch driver is a single
|
||||
* sequential scan pass within one thread, so this module-level flag is safe. */
|
||||
static bool batch_metadata_mode = false;
|
||||
|
||||
static bool write_all_bytes(int fd, const void* data, size_t size) {
|
||||
@@ -91,22 +96,37 @@ int batch_read_apply(int fd, const Config* config, const char* dest_root) {
|
||||
if (fd < 0 || dest_root == NULL || dest_root[0] == '\0')
|
||||
return -1;
|
||||
|
||||
/* Directory metadata is deferred to the end of the apply (a child write would
|
||||
* otherwise clobber its parent's mtime/mode). The batch header's single
|
||||
* metadata bit only says whether metadata is present in the stream; which
|
||||
* attributes are APPLIED comes from the invoking process's config, so
|
||||
* --read-batch must be invoked with the same -p/-t/-o/-g as the write side
|
||||
* (rsync requires the same options). The identity snapshot is activated so
|
||||
* -o/-g and the explicit ownership flags can apply. */
|
||||
DirTimeList dir_times;
|
||||
dir_time_list_init(&dir_times);
|
||||
int result = -1;
|
||||
if (!identity_set_active(config)) {
|
||||
log_message(LOG_LEVEL_ERROR, "batch: could not activate the identity policy");
|
||||
goto done;
|
||||
}
|
||||
|
||||
char magic[BATCH_MAGIC_LEN];
|
||||
bool eof = false;
|
||||
if (!read_exact(fd, magic, BATCH_MAGIC_LEN, &eof) || eof ||
|
||||
memcmp(magic, BATCH_MAGIC, BATCH_MAGIC_LEN) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "batch: malformed header (bad magic)");
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
unsigned char version;
|
||||
if (!read_exact(fd, &version, 1, &eof) || eof || version != BATCH_FORMAT_VERSION) {
|
||||
log_message(LOG_LEVEL_ERROR, "batch: malformed header (bad or missing format version)");
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
unsigned char mode;
|
||||
if (!read_exact(fd, &mode, 1, &eof) || eof || (mode != 0 && mode != 1)) {
|
||||
log_message(LOG_LEVEL_ERROR, "batch: malformed header (bad metadata flag)");
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
bool use_metadata = mode == 1;
|
||||
|
||||
@@ -114,47 +134,62 @@ int batch_read_apply(int fd, const Config* config, const char* dest_root) {
|
||||
unsigned long long length;
|
||||
if (!read_exact(fd, &length, sizeof(length), &eof)) {
|
||||
log_message(LOG_LEVEL_ERROR, "batch: truncated length prefix");
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
if (eof)
|
||||
break; /* clean end of stream */
|
||||
if (length == 0 || length > BATCH_MAX_RECORD) {
|
||||
log_message(LOG_LEVEL_ERROR, "batch: rejected record length %llu (valid range 1..%llu)",
|
||||
length, (unsigned long long)BATCH_MAX_RECORD);
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
char* record = (char*)malloc((size_t)length);
|
||||
if (record == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "batch: could not allocate a %llu-byte record", length);
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
if (!read_exact(fd, record, (size_t)length, &eof) || eof) {
|
||||
log_message(LOG_LEVEL_ERROR, "batch: truncated chunk record");
|
||||
free(record);
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
Data* data = data_create(record, (size_t)length);
|
||||
if (data == NULL)
|
||||
return -1; /* data_create frees `record` on failure */
|
||||
goto done; /* data_create frees `record` on failure */
|
||||
Chunk* chunk = chunk_deserialize(data, use_metadata);
|
||||
data_destroy(data);
|
||||
if (chunk == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "batch: rejected malformed chunk record");
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
File* file = chunk->items[i];
|
||||
chunk->items[i] = NULL;
|
||||
if (file == NULL)
|
||||
continue;
|
||||
FileSaveResult result = file_save_to_disk_full(dest_root, file, config);
|
||||
file_destroy(file);
|
||||
if (result == FILE_SAVE_ERROR) {
|
||||
FileSaveResult save = file_save_to_disk_full(dest_root, file, config);
|
||||
/* Accumulate directory metadata (when it applies) before the File is
|
||||
* destroyed; applied once the whole stream has been consumed. */
|
||||
if (save != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
||||
dir_metadata_should_capture(config) &&
|
||||
!dir_time_list_add(&dir_times, file->path, file->metadata)) {
|
||||
file_destroy(file);
|
||||
chunk_destroy(chunk);
|
||||
return -1;
|
||||
goto done;
|
||||
}
|
||||
file_destroy(file);
|
||||
if (save == FILE_SAVE_ERROR) {
|
||||
chunk_destroy(chunk);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
chunk_destroy(chunk);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
dir_metadata_list_apply(&dir_times, dest_root, config);
|
||||
result = 0;
|
||||
|
||||
done:
|
||||
identity_clear_active();
|
||||
dir_time_list_free(&dir_times);
|
||||
return result;
|
||||
}
|
||||
|
||||
+25
-1
@@ -20,6 +20,8 @@
|
||||
static void config_set_defaults(Config* config) {
|
||||
config->scanner_threads = 0;
|
||||
config->metadata_explicitly_disabled = false;
|
||||
config->preserve_perms_explicit_off = false;
|
||||
config->preserve_times_explicit_off = false;
|
||||
config->show_progress = false;
|
||||
config->compression_threads = 0;
|
||||
config->ssh_port = 22;
|
||||
@@ -194,7 +196,9 @@ static bool validate_received_config(const Config* config) {
|
||||
valid_wire_bool(config->dry_run) && checksum_algo_valid(config->checksum_algo) &&
|
||||
identity_wire_valid(config) && valid_wire_bool(config->preserve_atimes) &&
|
||||
valid_wire_bool(config->preserve_crtimes) && valid_wire_bool(config->omit_dir_times) &&
|
||||
valid_wire_bool(config->omit_link_times) && valid_wire_bool(config->munge_links) &&
|
||||
valid_wire_bool(config->omit_link_times) && valid_wire_bool(config->preserve_perms) &&
|
||||
valid_wire_bool(config->preserve_times) && valid_wire_bool(config->preserve_owner) &&
|
||||
valid_wire_bool(config->preserve_group) && 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 ||
|
||||
@@ -292,11 +296,31 @@ const char* config_invariants_error(const Config* config) {
|
||||
"timing; at most one may be given and each implies --delete";
|
||||
if (config->iconv_spec && !charset_spec_valid(config->iconv_spec))
|
||||
return "--iconv requires LOCAL[,REMOTE] charset names supported by iconv";
|
||||
if ((config->preserve_perms || config->preserve_times || config->preserve_owner ||
|
||||
config->preserve_group || config->preserve_atimes || config->preserve_crtimes ||
|
||||
config->use_executability) &&
|
||||
!config->use_metadata)
|
||||
return "a preservation attribute requires metadata transmission";
|
||||
if (config->copy_as_set && !config->use_metadata)
|
||||
return "--copy-as requires metadata preservation and cannot be combined with --no-preserve";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool config_derived_use_metadata(const Config* config) {
|
||||
if (!config)
|
||||
return false;
|
||||
if (config->preserve_perms || config->preserve_times || config->preserve_owner ||
|
||||
config->preserve_group || config->preserve_atimes || config->preserve_crtimes ||
|
||||
config->use_executability || config->preserve_xattrs || config->preserve_acls ||
|
||||
config->fake_super || config->preserve_devices || config->preserve_specials ||
|
||||
config->copy_devices || config->write_devices ||
|
||||
(config->chmod_spec && config->chmod_spec[0]) || config->copy_as_set ||
|
||||
config->chown_uid_set || config->chown_gid_set || config->usermap_count > 0 ||
|
||||
config->groupmap_count > 0 || config->update)
|
||||
return true;
|
||||
return (config->use_incremental || config->use_delta) && !config->metadata_explicitly_disabled;
|
||||
}
|
||||
|
||||
bool config_has_basis(const Config* config) {
|
||||
return config && config->basis_count > 0;
|
||||
}
|
||||
|
||||
+59
-5
@@ -76,7 +76,7 @@ typedef struct {
|
||||
typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF = 2 } SuperMode;
|
||||
|
||||
/* ===========================================================================
|
||||
* Config wire-field table (single source of truth for protocol 2.21.0).
|
||||
* Config wire-field table (single source of truth for protocol 2.22.0).
|
||||
*
|
||||
* Every field below crosses the wire. The table is the ONLY place a
|
||||
* serialized field is named: config.h expands CONFIG_WIRE_FIELDS() to declare
|
||||
@@ -216,7 +216,11 @@ typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF
|
||||
X(preserve_atimes, bool, false, BOOL) \
|
||||
X(preserve_crtimes, bool, false, BOOL) \
|
||||
X(omit_dir_times, bool, false, BOOL) \
|
||||
X(omit_link_times, bool, false, BOOL)
|
||||
X(omit_link_times, bool, false, BOOL) \
|
||||
X(preserve_perms, bool, false, BOOL) \
|
||||
X(preserve_times, bool, false, BOOL) \
|
||||
X(preserve_owner, bool, false, BOOL) \
|
||||
X(preserve_group, bool, false, BOOL)
|
||||
|
||||
#define CONFIG_WIRE_SYMLINK_TRUST_FIELDS(X) \
|
||||
X(munge_links, bool, false, BOOL) \
|
||||
@@ -266,6 +270,15 @@ typedef struct Config {
|
||||
* concern and is NEVER serialized into the wire config frame. */
|
||||
int scanner_threads;
|
||||
bool metadata_explicitly_disabled;
|
||||
/* CLIENT-ONLY (never serialized; not in CONFIG_WIRE_FIELDS). Set when the
|
||||
* user explicitly turned an attribute off with --no-perms / --no-times (long
|
||||
* or short form). --incremental/--delta historically auto-enabled mode and
|
||||
* mtime preservation; these flags let cli_finalize_config restore that
|
||||
* behavior while still honoring the explicit per-attribute negation. A
|
||||
* later -p/-t re-enables the attribute directly, so the flag only prevents
|
||||
* the incremental/delta implication, never a POSITIVE request. */
|
||||
bool preserve_perms_explicit_off;
|
||||
bool preserve_times_explicit_off;
|
||||
bool show_progress;
|
||||
int compression_threads;
|
||||
int ssh_port;
|
||||
@@ -579,6 +592,22 @@ typedef struct Config {
|
||||
/* -O/--omit-dir-times: do not apply mtimes to directories. */
|
||||
/* omit_link_times */
|
||||
/* -J/--omit-link-times: do not apply times to symlinks. */
|
||||
/* preserve_perms */
|
||||
/* -p/--perms: preserve the source permission bits (mode). One of the four
|
||||
* per-attribute preservation flags split out of the former single
|
||||
* use_metadata bundle; --chmod and -A/--acls also imply it. */
|
||||
/* preserve_times */
|
||||
/* -t/--times: preserve source modification times. Split out of the former
|
||||
* use_metadata bundle; --preserve and -a/--archive imply it. */
|
||||
/* preserve_owner */
|
||||
/* -o/--owner: preserve the source owner (uid). Split out of the former
|
||||
* use_metadata bundle; --usermap/--chown (and, when a uid is requested,
|
||||
* --copy-as) imply it. Owner application still requires receiver privilege
|
||||
* and is gated separately by the identity flags. */
|
||||
/* preserve_group */
|
||||
/* -g/--group: preserve the source group (gid). Split out of the former
|
||||
* use_metadata bundle; --groupmap/--chown (and, when a gid is requested,
|
||||
* --copy-as) imply it. */
|
||||
/* fake_super */
|
||||
/* --fake-super: receiver-only. When set, each written file additionally gets
|
||||
* a reserved user.fastsync.stat xattr recording the source uid/gid/mode/mtime
|
||||
@@ -623,7 +652,7 @@ typedef struct Config {
|
||||
* 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() and identity_ownership_requested() in
|
||||
* privilege_super_permitted() and identity_explicit_ownership_requested() in
|
||||
* identity.h. */
|
||||
/* copy_as_set */
|
||||
/* --copy-as=USER[:GROUP] (P7 Wave E, protocol 2.18.0). Safe-subset
|
||||
@@ -803,8 +832,25 @@ typedef struct Config {
|
||||
* unknown status, or the unconsumed detail body, and the strict same-version
|
||||
* handshake (config_receive rejects a mismatched version before parsing
|
||||
* anything else) is what keeps a 2.21 client and a 2.20 server from ever
|
||||
* reaching that state. */
|
||||
#define PROTOCOL_VERSION "2.21.0"
|
||||
* reaching that state.
|
||||
*
|
||||
* Preserve-Attribute Split Wave: 2.21.0 -> 2.22.0.
|
||||
*
|
||||
* WHY the bump, grounded in the wire: this wave splits the former single
|
||||
* use_metadata bundle into four independent rsync-compatible preservation
|
||||
* attributes (preserve_perms / preserve_times / preserve_owner /
|
||||
* preserve_group) so -p/-t/-o/-g (and their --no-* negations) become real
|
||||
* drop-in flags. The binary config frame gains four serialized bools appended
|
||||
* to CONFIG_WIRE_METADATA_TIMES_FIELDS after omit_link_times, in this fixed
|
||||
* order: preserve_perms, preserve_times, preserve_owner, preserve_group. 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.22 client and a 2.21
|
||||
* server from ever reaching that state. The fixed-width FileMetadata layout is
|
||||
* UNCHANGED: the receiver still gates attribute application on use_metadata,
|
||||
* which is now DERIVED from these attributes by config_derived_use_metadata(). */
|
||||
#define PROTOCOL_VERSION "2.22.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
|
||||
@@ -918,6 +964,14 @@ bool config_has_valid_delete_timing(const Config* config);
|
||||
* validate_received_config() so the receiver enforces exactly the same
|
||||
* invariants it relies on (the server is the trust boundary). */
|
||||
const char* config_invariants_error(const Config* config);
|
||||
/* Single source of truth for the DERIVED transport bit (use_metadata): true
|
||||
* when any configured preservation/ownership option requires the metadata
|
||||
* frame to travel. Returns false when no such option is set (a bare run).
|
||||
* This is a pure predicate over the config; the client lowers it into
|
||||
* Config->use_metadata at the end of parsing so every implication (devices,
|
||||
* executability, identity maps, incremental/delta, ...) is centralized here
|
||||
* rather than scattered as direct writes. */
|
||||
bool config_derived_use_metadata(const Config* config);
|
||||
/* True when at least one --compare-dest/--copy-dest/--link-dest was set. */
|
||||
bool config_has_basis(const Config* config);
|
||||
/* Append one basis-dir entry. Returns 0 on success, -1 on allocation failure. */
|
||||
|
||||
+145
-55
@@ -6,6 +6,7 @@
|
||||
#include <fcntl.h>
|
||||
#include <libgen.h>
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -72,6 +73,63 @@ static unsigned long long next_temp_sequence(void) {
|
||||
return atomic_fetch_add_explicit(&sequence, 1, memory_order_relaxed);
|
||||
}
|
||||
|
||||
/* Process-wide umask, captured exactly once. Reading the umask requires a
|
||||
* get+set round trip (umask(0); umask(old)); doing that per write would be racy
|
||||
* in the multithreaded receiver, so the value is captured at process startup by
|
||||
* file_umask_capture() (called at the top of main(), before any threads exist).
|
||||
* The pthread_once fallback keeps a caller that never called the capture (e.g. a
|
||||
* unit test) correct. */
|
||||
static unsigned g_process_umask;
|
||||
static atomic_bool g_process_umask_captured;
|
||||
static pthread_once_t g_process_umask_once = PTHREAD_ONCE_INIT;
|
||||
|
||||
static void file_capture_umask_now(void) {
|
||||
mode_t mask = umask(0);
|
||||
umask(mask);
|
||||
g_process_umask = (unsigned)mask;
|
||||
atomic_store_explicit(&g_process_umask_captured, true, memory_order_release);
|
||||
}
|
||||
|
||||
static void file_capture_umask_once(void) {
|
||||
if (atomic_load_explicit(&g_process_umask_captured, memory_order_acquire))
|
||||
return;
|
||||
file_capture_umask_now();
|
||||
}
|
||||
|
||||
/* Re-captures the umask. Must only be called while the process is still
|
||||
* single-threaded (startup, or the daemon's post-fork setup after umask(0)),
|
||||
* so a later re-capture can refresh the cached value before any receiver
|
||||
* thread exists. */
|
||||
void file_umask_capture(void) {
|
||||
file_capture_umask_now();
|
||||
}
|
||||
|
||||
unsigned file_process_umask(void) {
|
||||
if (!atomic_load_explicit(&g_process_umask_captured, memory_order_acquire))
|
||||
pthread_once(&g_process_umask_once, file_capture_umask_once);
|
||||
return g_process_umask;
|
||||
}
|
||||
|
||||
/* Base mode applied when the policy does not take the source mode wholesale
|
||||
* (i.e. --perms is off). A pre-existing destination keeps its own mode; a
|
||||
* brand-new file is created like rsync: source_mode & 0777 & ~umask, with
|
||||
* S_IWGRP|S_IWOTH always cleared so a client mode can never grant group/other
|
||||
* write (the daemon runs with umask(0)). Only when no metadata is available at
|
||||
* all does the historical fixed 0644 default apply. The -E rule (and no-op for
|
||||
* a plain -t) is layered on top of this base. */
|
||||
static mode_t file_mode_base(const FileMetadata* metadata, bool existing_known,
|
||||
mode_t existing_mode) {
|
||||
if (existing_known)
|
||||
return existing_mode;
|
||||
if (metadata)
|
||||
/* A brand-new file follows rsync's source_mode & ~umask base, but a
|
||||
* client-supplied source mode must never grant group/other write (the
|
||||
* daemon runs with umask(0), so an unmasked 0666 would otherwise create a
|
||||
* world-writable file). S_IWGRP|S_IWOTH are always cleared. */
|
||||
return metadata->mode & 0777 & ~(mode_t)file_process_umask() & ~(S_IWGRP | S_IWOTH);
|
||||
return S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
|
||||
}
|
||||
|
||||
bool file_checksum(File* file, ChecksumAlgo algo, uint64_t seed, uint8_t* out, size_t out_capacity,
|
||||
size_t* out_len) {
|
||||
if (!file || !out || !out_len || !file->data)
|
||||
@@ -861,7 +919,7 @@ int file_open_private_dir(const char* dir_path) {
|
||||
* destination file) and best-effort: a per-attribute or privilege failure is
|
||||
* logged and skipped, never fatal. */
|
||||
static void restore_extra_fd(int fd, const FileMetadata* metadata, const FileXattrList* xattrs,
|
||||
bool fake_super) {
|
||||
bool fake_super, FileAttrPolicy policy) {
|
||||
xattr_apply_fd(fd, xattrs);
|
||||
if (fake_super && metadata) {
|
||||
fake_super_store_fd(fd, (uint32_t)metadata->uid, (uint32_t)metadata->gid,
|
||||
@@ -869,15 +927,16 @@ static void restore_extra_fd(int fd, const FileMetadata* metadata, const FileXat
|
||||
/* Replay: re-apply the recorded uid/gid/mode/mtime fd-relative so a save
|
||||
under --fake-super restores the attrs (when privileged) instead of only
|
||||
recording them. Best-effort; fake_super_restore_fd silently skips a
|
||||
non-root fchown EPERM/EACCES and never fatal. */
|
||||
fake_super_restore_fd(fd);
|
||||
non-root fchown EPERM/EACCES and never fatal. The replayed mode/mtime
|
||||
honor the per-attribute policy so fake-super cannot bypass the split. */
|
||||
fake_super_restore_fd(fd, policy);
|
||||
}
|
||||
}
|
||||
|
||||
static bool file_to_disk_secure_impl(const char* path, const void* data,
|
||||
unsigned long long data_size, bool inplace, bool sparse,
|
||||
bool preallocate, const FileMetadata* metadata,
|
||||
bool preserve_executability, bool update, bool no_replace,
|
||||
FileAttrPolicy policy, bool update, bool no_replace,
|
||||
bool use_fsync, const char* temp_dir,
|
||||
const FileXattrList* xattrs, bool fake_super,
|
||||
bool keep_partial) {
|
||||
@@ -887,6 +946,13 @@ static bool file_to_disk_secure_impl(const char* path, const void* data,
|
||||
return false;
|
||||
int fd = -1;
|
||||
bool ok = false;
|
||||
/* The base mode applied when --perms is off (neither the source mode nor an
|
||||
* exec-only change is taken wholesale): a pre-existing destination keeps its
|
||||
* own mode (special bits dropped), while a brand-new file uses
|
||||
* source&~umask when metadata is available (see file_mode_base) or 0644 when
|
||||
* there is none. Captured from the destination probe before the write. */
|
||||
mode_t existing_mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
|
||||
bool existing_mode_known = false;
|
||||
if (inplace) {
|
||||
/* --inplace writes directly into the destination; a scratch --temp-dir
|
||||
does not apply and must never redirect these writes. */
|
||||
@@ -897,10 +963,16 @@ static bool file_to_disk_secure_impl(const char* path, const void* data,
|
||||
super-mode gate (a client-controlled device write). fstatat with
|
||||
AT_SYMLINK_NOFOLLOW does not follow a symlink and does not block. */
|
||||
struct stat pre_stat;
|
||||
if (fstatat(dirfd, leaf, &pre_stat, AT_SYMLINK_NOFOLLOW) == 0 && !S_ISREG(pre_stat.st_mode)) {
|
||||
close(dirfd);
|
||||
free(leaf);
|
||||
return false;
|
||||
if (fstatat(dirfd, leaf, &pre_stat, AT_SYMLINK_NOFOLLOW) == 0) {
|
||||
if (!S_ISREG(pre_stat.st_mode)) {
|
||||
close(dirfd);
|
||||
free(leaf);
|
||||
return false;
|
||||
}
|
||||
/* Capture the old destination mode before the overwrite so a no--p/-E
|
||||
* write can restore it (the write itself may clear setuid/setgid). */
|
||||
existing_mode = pre_stat.st_mode & 0777;
|
||||
existing_mode_known = true;
|
||||
}
|
||||
/* O_NONBLOCK: a no-op for a regular file, but a raced-in FIFO cannot block
|
||||
the open before the post-open S_ISREG re-check rejects it. */
|
||||
@@ -953,15 +1025,25 @@ static bool file_to_disk_secure_impl(const char* path, const void* data,
|
||||
/* Normalize the mode: apply the metadata-derived safe mode when the
|
||||
sender supplied metadata (setuid/setgid/sticky are never honored);
|
||||
otherwise fall back to a safe default so dangerous bits on an
|
||||
existing destination cannot survive an overwrite. */
|
||||
existing destination cannot survive an overwrite. When the policy
|
||||
requests neither -p nor -E the source mode is deliberately ignored
|
||||
and the pre-existing destination mode (or 0644 for a new file) is
|
||||
restored instead. The exec-bits-only -E change is likewise applied
|
||||
on top of that destination-derived base, not the scratch file's
|
||||
0600. */
|
||||
if (ok) {
|
||||
if (metadata)
|
||||
ok = file_restore_metadata_fd(fd, metadata, preserve_executability);
|
||||
else if (fchmod(fd, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) != 0)
|
||||
if (metadata) {
|
||||
if (!policy.perms &&
|
||||
fchmod(fd, file_mode_base(metadata, existing_mode_known, existing_mode)) != 0)
|
||||
ok = false;
|
||||
if (ok)
|
||||
ok = file_restore_metadata_fd(fd, metadata, policy);
|
||||
} else if (fchmod(fd, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) != 0) {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (ok)
|
||||
restore_extra_fd(fd, metadata, xattrs, fake_super);
|
||||
restore_extra_fd(fd, metadata, xattrs, fake_super, policy);
|
||||
if (ok && use_fsync)
|
||||
ok = fsync(fd) == 0;
|
||||
}
|
||||
@@ -974,16 +1056,21 @@ static bool file_to_disk_secure_impl(const char* path, const void* data,
|
||||
install failure (partial data may exist, --partial may retain it) from a
|
||||
pre-write validation failure (nothing to retain). */
|
||||
bool write_attempted = false;
|
||||
if (update && metadata) {
|
||||
/* This check protects the normal atomic path as far as possible. A
|
||||
concurrent replacement can still occur before the final rename. */
|
||||
struct stat destination_stat;
|
||||
if (fstatat(dirfd, leaf, &destination_stat, AT_SYMLINK_NOFOLLOW) == 0 &&
|
||||
S_ISREG(destination_stat.st_mode) && stat_is_newer(&destination_stat, metadata)) {
|
||||
close(dirfd);
|
||||
free(leaf);
|
||||
return true;
|
||||
}
|
||||
/* Probe the destination ONCE up front: it both drives the --update check
|
||||
and records the pre-existing mode the no--p/-E fallback preserves. */
|
||||
struct stat destination_stat;
|
||||
bool destination_is_regular =
|
||||
fstatat(dirfd, leaf, &destination_stat, AT_SYMLINK_NOFOLLOW) == 0 &&
|
||||
S_ISREG(destination_stat.st_mode);
|
||||
if (destination_is_regular) {
|
||||
existing_mode = destination_stat.st_mode & 0777;
|
||||
existing_mode_known = true;
|
||||
}
|
||||
if (update && metadata && destination_is_regular &&
|
||||
stat_is_newer(&destination_stat, metadata)) {
|
||||
close(dirfd);
|
||||
free(leaf);
|
||||
return true;
|
||||
}
|
||||
/* Scratch directory for the temporary working copy. When NULL the temp
|
||||
file is created in the destination directory, exactly as historically. */
|
||||
@@ -1061,10 +1148,19 @@ static bool file_to_disk_secure_impl(const char* path, const void* data,
|
||||
? file_store_write_sparse(fd, (const unsigned char*)data, data_size)
|
||||
: write_all(fd, data, data_size);
|
||||
}
|
||||
if (ok && metadata)
|
||||
ok = file_restore_metadata_fd(fd, metadata, preserve_executability);
|
||||
if (ok) {
|
||||
if (metadata) {
|
||||
if (!policy.perms &&
|
||||
fchmod(fd, file_mode_base(metadata, existing_mode_known, existing_mode)) != 0)
|
||||
ok = false;
|
||||
if (ok)
|
||||
ok = file_restore_metadata_fd(fd, metadata, policy);
|
||||
} else if (fchmod(fd, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) != 0) {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (ok)
|
||||
restore_extra_fd(fd, metadata, xattrs, fake_super);
|
||||
restore_extra_fd(fd, metadata, xattrs, fake_super, policy);
|
||||
if (ok && use_fsync)
|
||||
ok = fsync(fd) == 0;
|
||||
}
|
||||
@@ -1129,38 +1225,33 @@ static bool file_to_disk_secure_impl(const char* path, const void* data,
|
||||
|
||||
bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size,
|
||||
bool inplace, bool sparse, bool preallocate, const FileMetadata* metadata,
|
||||
bool preserve_executability, const char* temp_dir) {
|
||||
FileAttrPolicy policy, const char* temp_dir) {
|
||||
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
|
||||
preserve_executability, false, false, false, temp_dir, NULL,
|
||||
false, false);
|
||||
policy, false, false, false, temp_dir, NULL, false, false);
|
||||
}
|
||||
|
||||
bool file_to_disk_secure_update(const char* path, const void* data, unsigned long long data_size,
|
||||
bool inplace, bool sparse, bool preallocate,
|
||||
const FileMetadata* metadata, bool preserve_executability,
|
||||
const FileMetadata* metadata, FileAttrPolicy policy,
|
||||
const char* temp_dir) {
|
||||
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
|
||||
preserve_executability, true, false, false, temp_dir, NULL, false,
|
||||
false);
|
||||
policy, true, false, false, temp_dir, NULL, false, false);
|
||||
}
|
||||
|
||||
bool file_to_disk_secure_with_fsync(const char* path, const void* data,
|
||||
unsigned long long data_size, bool inplace, bool sparse,
|
||||
bool preallocate, const FileMetadata* metadata,
|
||||
bool preserve_executability, bool use_fsync,
|
||||
const char* temp_dir) {
|
||||
FileAttrPolicy policy, bool use_fsync, const char* temp_dir) {
|
||||
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
|
||||
preserve_executability, false, false, use_fsync, temp_dir, NULL,
|
||||
false, false);
|
||||
policy, false, false, use_fsync, temp_dir, NULL, false, false);
|
||||
}
|
||||
|
||||
bool file_to_disk_secure_no_replace(const char* path, const void* data,
|
||||
unsigned long long data_size, bool sparse, bool preallocate,
|
||||
const FileMetadata* metadata, bool preserve_executability,
|
||||
const FileMetadata* metadata, FileAttrPolicy policy,
|
||||
const char* temp_dir) {
|
||||
return file_to_disk_secure_impl(path, data, data_size, false, sparse, preallocate, metadata,
|
||||
preserve_executability, false, true, false, temp_dir, NULL, false,
|
||||
false);
|
||||
policy, false, true, false, temp_dir, NULL, false, false);
|
||||
}
|
||||
|
||||
/* Receiver write-path variant that also applies the per-file xattrs (-X/-A)
|
||||
@@ -1170,13 +1261,12 @@ bool file_to_disk_secure_no_replace(const char* path, const void* data,
|
||||
* failed write's temp. See file_to_disk_secure_impl for the semantics. */
|
||||
bool file_to_disk_secure_attrs(const char* path, const void* data, unsigned long long data_size,
|
||||
bool inplace, bool sparse, bool preallocate,
|
||||
const FileMetadata* metadata, bool preserve_executability,
|
||||
bool update, bool no_replace, bool use_fsync,
|
||||
const FileXattrList* xattrs, bool fake_super, bool keep_partial,
|
||||
const char* temp_dir) {
|
||||
const FileMetadata* metadata, FileAttrPolicy policy, bool update,
|
||||
bool no_replace, bool use_fsync, const FileXattrList* xattrs,
|
||||
bool fake_super, bool keep_partial, const char* temp_dir) {
|
||||
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
|
||||
preserve_executability, update, no_replace, use_fsync, temp_dir,
|
||||
xattrs, fake_super, keep_partial);
|
||||
policy, update, no_replace, use_fsync, temp_dir, xattrs,
|
||||
fake_super, keep_partial);
|
||||
}
|
||||
|
||||
/* Atomic --link-dest install. The destination is replaced (via a temporary
|
||||
@@ -1197,7 +1287,7 @@ bool file_to_disk_secure_attrs(const char* path, const void* data, unsigned long
|
||||
static bool file_to_disk_secure_link_impl(const char* path, const char* basis_path,
|
||||
const void* data, unsigned long long data_size,
|
||||
bool preallocate, const FileMetadata* metadata,
|
||||
bool preserve_executability, bool use_fsync,
|
||||
FileAttrPolicy policy, bool use_fsync,
|
||||
const FileXattrList* xattrs, bool fake_super,
|
||||
const char* temp_dir) {
|
||||
if (!path || !basis_path)
|
||||
@@ -1286,8 +1376,8 @@ static bool file_to_disk_secure_link_impl(const char* path, const char* basis_pa
|
||||
/* The basis file could not be linked in (missing, cross-device, refused
|
||||
by the filesystem). Write a byte-identical local copy instead. */
|
||||
return file_to_disk_secure_attrs(path, data, data_size, false, false, preallocate, metadata,
|
||||
preserve_executability, false, false, use_fsync, xattrs,
|
||||
fake_super, false, temp_dir);
|
||||
policy, false, false, use_fsync, xattrs, fake_super, false,
|
||||
temp_dir);
|
||||
}
|
||||
|
||||
if (scratch_dirfd >= 0)
|
||||
@@ -1299,25 +1389,25 @@ static bool file_to_disk_secure_link_impl(const char* path, const char* basis_pa
|
||||
|
||||
bool file_to_disk_secure_link(const char* path, const char* basis_path, const void* data,
|
||||
unsigned long long data_size, bool preallocate,
|
||||
const FileMetadata* metadata, bool preserve_executability,
|
||||
bool use_fsync, const char* temp_dir) {
|
||||
const FileMetadata* metadata, FileAttrPolicy policy, bool use_fsync,
|
||||
const char* temp_dir) {
|
||||
return file_to_disk_secure_link_impl(path, basis_path, data, data_size, preallocate, metadata,
|
||||
preserve_executability, use_fsync, NULL, false, temp_dir);
|
||||
policy, use_fsync, NULL, false, temp_dir);
|
||||
}
|
||||
|
||||
bool file_to_disk_secure_link_attrs(const char* path, const char* basis_path, const void* data,
|
||||
unsigned long long data_size, bool preallocate,
|
||||
const FileMetadata* metadata, bool preserve_executability,
|
||||
const FileMetadata* metadata, FileAttrPolicy policy,
|
||||
bool use_fsync, const FileXattrList* xattrs, bool fake_super,
|
||||
const char* temp_dir) {
|
||||
return file_to_disk_secure_link_impl(path, basis_path, data, data_size, preallocate, metadata,
|
||||
preserve_executability, use_fsync, xattrs, fake_super,
|
||||
temp_dir);
|
||||
policy, use_fsync, xattrs, fake_super, temp_dir);
|
||||
}
|
||||
|
||||
bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size,
|
||||
bool inplace, bool sparse) {
|
||||
if (!path || (!data && data_size != 0) || has_path_traversal(path))
|
||||
return false;
|
||||
return file_to_disk_secure(path, data, data_size, inplace, sparse, false, NULL, false, NULL);
|
||||
FileAttrPolicy policy = {false, false, false, false};
|
||||
return file_to_disk_secure(path, data, data_size, inplace, sparse, false, NULL, policy, NULL);
|
||||
}
|
||||
|
||||
+21
-12
@@ -28,6 +28,17 @@ void file_metadata_destroy(void* metadata);
|
||||
/* --open-noatime process-wide sender policy; see file.c. */
|
||||
void file_set_open_noatime(bool enable);
|
||||
bool file_get_open_noatime(void);
|
||||
/* Capture the process umask ONCE, before any threads are created. Call this at
|
||||
* the very top of main() in both entry points so the cached value is read while
|
||||
* the process is still single-threaded: reading the umask needs a get+set round
|
||||
* trip (umask(0); umask(old)), which would race against receiver threads
|
||||
* creating files if it happened during the first write. Idempotent and safe to
|
||||
* call more than once. */
|
||||
void file_umask_capture(void);
|
||||
/* Process-wide umask, captured once (thread-safe). Used to derive the mode of
|
||||
* a brand-new destination like rsync: source_mode & 0777 & ~umask. Falls back
|
||||
* to file_umask_capture() (behind pthread_once) if capture was never called. */
|
||||
unsigned file_process_umask(void);
|
||||
/* Open `path` read-only for transfer, honouring --open-noatime when set. */
|
||||
int file_open_for_read(const char* path);
|
||||
bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size,
|
||||
@@ -90,22 +101,21 @@ int file_open_private_dir(const char* dir_path);
|
||||
behavior. --inplace writes never use temp_dir. */
|
||||
bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size,
|
||||
bool inplace, bool sparse, bool preallocate, const FileMetadata* metadata,
|
||||
bool preserve_executability, const char* temp_dir);
|
||||
FileAttrPolicy policy, const char* temp_dir);
|
||||
bool file_to_disk_secure_with_fsync(const char* path, const void* data,
|
||||
unsigned long long data_size, bool inplace, bool sparse,
|
||||
bool preallocate, const FileMetadata* metadata,
|
||||
bool preserve_executability, bool use_fsync,
|
||||
const char* temp_dir);
|
||||
FileAttrPolicy policy, bool use_fsync, const char* temp_dir);
|
||||
/* With update enabled, an existing newer destination is left untouched. The
|
||||
check is descriptor-based for inplace writes; atomic replacement still has
|
||||
an unavoidable final rename race without filesystem locking. */
|
||||
bool file_to_disk_secure_update(const char* path, const void* data, unsigned long long data_size,
|
||||
bool inplace, bool sparse, bool preallocate,
|
||||
const FileMetadata* metadata, bool preserve_executability,
|
||||
const FileMetadata* metadata, FileAttrPolicy policy,
|
||||
const char* temp_dir);
|
||||
bool file_to_disk_secure_no_replace(const char* path, const void* data,
|
||||
unsigned long long data_size, bool sparse, bool preallocate,
|
||||
const FileMetadata* metadata, bool preserve_executability,
|
||||
const FileMetadata* metadata, FileAttrPolicy policy,
|
||||
const char* temp_dir);
|
||||
/* Receiver write-path variant that also applies per-file xattrs (-X/-A) and the
|
||||
* --fake-super stat xattr fd-relative before the final rename. `update` /
|
||||
@@ -113,10 +123,9 @@ bool file_to_disk_secure_no_replace(const char* path, const void* data,
|
||||
* enables --partial best-effort retention of a failed write's temp. */
|
||||
bool file_to_disk_secure_attrs(const char* path, const void* data, unsigned long long data_size,
|
||||
bool inplace, bool sparse, bool preallocate,
|
||||
const FileMetadata* metadata, bool preserve_executability,
|
||||
bool update, bool no_replace, bool use_fsync,
|
||||
const FileXattrList* xattrs, bool fake_super, bool keep_partial,
|
||||
const char* temp_dir);
|
||||
const FileMetadata* metadata, FileAttrPolicy policy, bool update,
|
||||
bool no_replace, bool use_fsync, const FileXattrList* xattrs,
|
||||
bool fake_super, bool keep_partial, const char* temp_dir);
|
||||
/* Atomic --link-dest install: replace `path` with a hard link to `basis_path`
|
||||
(via a temp name + rename); fall back to a byte-identical local copy from
|
||||
`data` when the link is impossible (EXDEV/EPERM/unsupported filesystem).
|
||||
@@ -125,15 +134,15 @@ bool file_to_disk_secure_attrs(const char* path, const void* data, unsigned long
|
||||
never re-allocated). */
|
||||
bool file_to_disk_secure_link(const char* path, const char* basis_path, const void* data,
|
||||
unsigned long long data_size, bool preallocate,
|
||||
const FileMetadata* metadata, bool preserve_executability,
|
||||
bool use_fsync, const char* temp_dir);
|
||||
const FileMetadata* metadata, FileAttrPolicy policy, bool use_fsync,
|
||||
const char* temp_dir);
|
||||
/* Like file_to_disk_secure_link, but the byte-copy fallback also applies the
|
||||
* per-file xattrs (-X/-A) and --fake-super stat xattr (fd-relative). On a
|
||||
* successful hard link no attributes are applied (the shared inode already
|
||||
* carries the basis's). */
|
||||
bool file_to_disk_secure_link_attrs(const char* path, const char* basis_path, const void* data,
|
||||
unsigned long long data_size, bool preallocate,
|
||||
const FileMetadata* metadata, bool preserve_executability,
|
||||
const FileMetadata* metadata, FileAttrPolicy policy,
|
||||
bool use_fsync, const FileXattrList* xattrs, bool fake_super,
|
||||
const char* temp_dir);
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#ifndef FILE_ATTR_H
|
||||
#define FILE_ATTR_H
|
||||
|
||||
#include "config.h"
|
||||
#include <stdbool.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/*
|
||||
* Per-attribute receiver policy for applying a transmitted FileMetadata. This
|
||||
* is the split-out replacement for the former single use_metadata bundle: each
|
||||
* flag is applied independently, matching rsync's -p/-t/-o/-g/-E/-U semantics.
|
||||
* `use_metadata` remains the transport/presence gate (whether the metadata frame
|
||||
* travelled at all); this struct decides which attributes are ACTUALLY applied.
|
||||
*
|
||||
* It lives in its own header (rather than metadata.h) because xattr.h's
|
||||
* fake_super_restore_fd() takes one and metadata.h <-> file_types.h form an
|
||||
* include cycle that must not be entered from xattr.h.
|
||||
*
|
||||
* The mode leg is: perms wins over executability; an exec-bits-only change is
|
||||
* made only when perms is off; when neither is set the receiver deliberately
|
||||
* sets no source mode. file.c then substitutes the pre-existing destination
|
||||
* mode for a brand-new destination with metadata it uses the sanitized
|
||||
* source-mode-&-umask base (S_IWGRP|S_IWOTH cleared), and the fixed 0644
|
||||
* default only when no metadata is available at all, so a no--p overwrite
|
||||
* does not lose the destination's perms.
|
||||
*/
|
||||
typedef struct FileAttrPolicy {
|
||||
bool perms; /* config->preserve_perms: apply the source mode bits */
|
||||
bool times; /* config->preserve_times: apply the source mtime */
|
||||
bool atimes; /* config->preserve_atimes (-U): apply the source atime */
|
||||
bool executability; /* config->use_executability (-E): exec-bits-only mode */
|
||||
} FileAttrPolicy;
|
||||
|
||||
/* Build the per-attribute policy from a connection's Config. A NULL config
|
||||
* yields the all-off policy (no attribute application). */
|
||||
FileAttrPolicy file_attr_policy_from_config(const Config* config);
|
||||
|
||||
#endif
|
||||
+112
-47
@@ -52,7 +52,7 @@ static FileSaveResult file_stage_delayed_update(const char* root_directory,
|
||||
if (!config)
|
||||
return FILE_SAVE_ERROR;
|
||||
bool sparse = config->preserve_sparse;
|
||||
bool preserve_executability = config->use_executability;
|
||||
FileAttrPolicy policy = file_attr_policy_from_config(config);
|
||||
|
||||
if (config->existing && !file_path_exists_secure(destination_path))
|
||||
return FILE_SAVE_SKIPPED;
|
||||
@@ -91,13 +91,12 @@ static FileSaveResult file_stage_delayed_update(const char* root_directory,
|
||||
bool ok;
|
||||
if (file->basis_link) {
|
||||
ok = file_to_disk_secure_link(staged_path, file->basis_link, file->data->data, file->data->size,
|
||||
config->preallocate, metadata, preserve_executability,
|
||||
config->use_fsync, NULL);
|
||||
config->preallocate, metadata, policy, config->use_fsync, NULL);
|
||||
} else {
|
||||
ok = file_to_disk_secure_attrs(staged_path, file->data->data, file->data->size, false, sparse,
|
||||
config->preallocate, metadata, preserve_executability, false,
|
||||
false, config->use_fsync, file->xattrs, config->fake_super,
|
||||
false, NULL);
|
||||
ok =
|
||||
file_to_disk_secure_attrs(staged_path, file->data->data, file->data->size, false, sparse,
|
||||
config->preallocate, metadata, policy, false, false,
|
||||
config->use_fsync, file->xattrs, config->fake_super, false, NULL);
|
||||
}
|
||||
if (!ok) {
|
||||
free(staged_path);
|
||||
@@ -229,7 +228,7 @@ static FileSaveResult file_save_hardlink_sibling(const char* root_directory, con
|
||||
}
|
||||
|
||||
bool preallocate = cfg && cfg->preallocate;
|
||||
bool preserve_executability = cfg && cfg->use_executability;
|
||||
FileAttrPolicy policy = file_attr_policy_from_config(cfg);
|
||||
bool use_fsync = cfg && cfg->use_fsync;
|
||||
|
||||
if (cfg->delay_updates) {
|
||||
@@ -265,9 +264,9 @@ static FileSaveResult file_save_hardlink_sibling(const char* root_directory, con
|
||||
}
|
||||
FileXattrList* sibling_xattrs =
|
||||
cfg->use_xattrs ? xattr_capture_path(staged_first, cfg->preserve_acls) : NULL;
|
||||
bool ok = file_to_disk_secure_link_attrs(
|
||||
staged_sibling, staged_first, content, content_size, preallocate, file->metadata,
|
||||
preserve_executability, use_fsync, sibling_xattrs, cfg ? cfg->fake_super : false, NULL);
|
||||
bool ok = file_to_disk_secure_link_attrs(staged_sibling, staged_first, content, content_size,
|
||||
preallocate, file->metadata, policy, use_fsync,
|
||||
sibling_xattrs, cfg ? cfg->fake_super : false, NULL);
|
||||
xattr_list_free(sibling_xattrs);
|
||||
free(content);
|
||||
if (ok)
|
||||
@@ -298,9 +297,9 @@ static FileSaveResult file_save_hardlink_sibling(const char* root_directory, con
|
||||
const char* temp_dir = (cfg && cfg->temp_dir) ? cfg->temp_dir : NULL;
|
||||
FileXattrList* sibling_xattrs =
|
||||
cfg->use_xattrs ? xattr_capture_path(first_disk, cfg->preserve_acls) : NULL;
|
||||
bool ok = file_to_disk_secure_link_attrs(
|
||||
destination_path, first_disk, content, content_size, preallocate, file->metadata,
|
||||
preserve_executability, use_fsync, sibling_xattrs, cfg ? cfg->fake_super : false, temp_dir);
|
||||
bool ok = file_to_disk_secure_link_attrs(destination_path, first_disk, content, content_size,
|
||||
preallocate, file->metadata, policy, use_fsync,
|
||||
sibling_xattrs, cfg ? cfg->fake_super : false, temp_dir);
|
||||
xattr_list_free(sibling_xattrs);
|
||||
free(content);
|
||||
free(first_disk);
|
||||
@@ -437,7 +436,10 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
} else {
|
||||
create_mode = S_IFIFO;
|
||||
}
|
||||
mode_t perms = mode & 0777;
|
||||
/* The creation permission bits come from the source only under -p/--perms;
|
||||
* otherwise a safe default (0644, group/other write never granted) keeps an
|
||||
* unprivileged no--p run from materializing a world-writable node. */
|
||||
mode_t perms = config->preserve_perms ? (mode & 0777 & ~(S_IWGRP | S_IWOTH)) : 0644;
|
||||
|
||||
int rc = is_fifo ? mkfifoat(parent_fd, leaf, perms)
|
||||
: mknodat(parent_fd, leaf, create_mode | perms, rdev);
|
||||
@@ -481,11 +483,23 @@ 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). */
|
||||
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);
|
||||
/* Apply times on the fresh node (utimensat, no-follow) per the negotiated
|
||||
* per-attribute policy: mtime only under -t, atime only under -U. The slot
|
||||
* not requested stays UTIME_OMIT so it is left untouched. */
|
||||
FileAttrPolicy policy = file_attr_policy_from_config(config);
|
||||
if (policy.times || (policy.atimes && file->metadata->atime_valid)) {
|
||||
struct timespec times[2] = {{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
|
||||
{.tv_sec = 0, .tv_nsec = UTIME_OMIT}};
|
||||
if (policy.times) {
|
||||
times[1].tv_sec = file->metadata->mtime_sec;
|
||||
times[1].tv_nsec = file->metadata->mtime_nsec;
|
||||
}
|
||||
if (policy.atimes && file->metadata->atime_valid) {
|
||||
times[0].tv_sec = file->metadata->atime_sec;
|
||||
times[0].tv_nsec = file->metadata->atime_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
|
||||
@@ -592,7 +606,7 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
bool backup_enabled = config && config->backup && !config->ignore_existing;
|
||||
bool inplace = config && config->inplace;
|
||||
bool sparse = config && config->preserve_sparse;
|
||||
bool preserve_executability = config && config->use_executability;
|
||||
FileAttrPolicy policy = file_attr_policy_from_config(config);
|
||||
const char* backup_suffix = (config && config->suffix) ? config->suffix : "~";
|
||||
const char* backup_dir = (config && config->backup_dir) ? config->backup_dir : NULL;
|
||||
const char* partial_dir = (config && config->partial_dir) ? config->partial_dir : NULL;
|
||||
@@ -734,8 +748,11 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
(utimensat/lchown/fchmodat AT_SYMLINK_NOFOLLOW). -J/--omit-link-times
|
||||
suppresses the timestamps; ownership stays gated by the identity policy.
|
||||
A symlink has no children, so this can be applied immediately. */
|
||||
if (ok && config && config->use_metadata)
|
||||
ok = file_restore_symlink_metadata(link_path, file->metadata, config->omit_link_times);
|
||||
if (ok && config && config->use_metadata) {
|
||||
FileAttrPolicy link_policy = file_attr_policy_from_config(config);
|
||||
ok = file_restore_symlink_metadata(link_path, file->metadata, link_policy,
|
||||
config->omit_link_times);
|
||||
}
|
||||
free(link_path);
|
||||
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
|
||||
}
|
||||
@@ -904,16 +921,15 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
policy decision. */
|
||||
bool ok;
|
||||
if (config && file->basis_link) {
|
||||
ok = file_to_disk_secure_link_attrs(disk_path, file->basis_link, file->data->data,
|
||||
file->data->size, config->preallocate, metadata,
|
||||
preserve_executability, config->use_fsync, file->xattrs,
|
||||
config->fake_super, confined_temp);
|
||||
ok = file_to_disk_secure_link_attrs(
|
||||
disk_path, file->basis_link, file->data->data, file->data->size, config->preallocate,
|
||||
metadata, policy, config->use_fsync, file->xattrs, config->fake_super, confined_temp);
|
||||
} else {
|
||||
/* The plain no-replace / update / with-fsync engines, plus per-file xattr
|
||||
(-X/-A) and --fake-super application on the written fd. */
|
||||
ok = file_to_disk_secure_attrs(
|
||||
disk_path, file->data->data, file->data->size, inplace, sparse,
|
||||
config && config->preallocate, metadata, preserve_executability, config && config->update,
|
||||
config && config->preallocate, metadata, policy, config && config->update,
|
||||
config && config->ignore_existing, config && config->use_fsync, file->xattrs,
|
||||
config ? config->fake_super : false, config ? config->partial : false, confined_temp);
|
||||
}
|
||||
@@ -2401,10 +2417,15 @@ File* file_receive(const Config* config, int file_descriptor) {
|
||||
return file;
|
||||
}
|
||||
|
||||
/* ---- P7 Wave D: deferred directory times ---- */
|
||||
/* ---- P7 Wave D: deferred directory metadata ---- */
|
||||
|
||||
bool dir_times_should_capture(const Config* config) {
|
||||
return config->use_metadata && !config->omit_dir_times;
|
||||
bool dir_metadata_should_capture(const Config* config) {
|
||||
/* Directory metadata is captured when a directory attribute is actually
|
||||
* requested: -p/--perms (directory modes) or -t/--times (directory mtimes,
|
||||
* unless -O/--omit-dir-times suppresses them). --atimes/-U alone does not
|
||||
* pull directory metadata (matching the original dir-time bundle). */
|
||||
return config && config->use_metadata &&
|
||||
(config->preserve_perms || (config->preserve_times && !config->omit_dir_times));
|
||||
}
|
||||
|
||||
void dir_time_list_init(DirTimeList* list) {
|
||||
@@ -2475,8 +2496,13 @@ bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetad
|
||||
return true;
|
||||
}
|
||||
|
||||
void dir_time_list_apply(const DirTimeList* list, const char* root_directory) {
|
||||
if (!list || !root_directory)
|
||||
void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory,
|
||||
const Config* config) {
|
||||
if (!list || !root_directory || !config)
|
||||
return;
|
||||
bool apply_times = config->preserve_times && !config->omit_dir_times;
|
||||
bool apply_mode = config->preserve_perms;
|
||||
if (!apply_times && !apply_mode)
|
||||
return;
|
||||
for (size_t i = 0; i < list->count; i++) {
|
||||
char* dir_path = path_cat(root_directory, list->paths[i]);
|
||||
@@ -2484,7 +2510,8 @@ void dir_time_list_apply(const DirTimeList* list, const char* root_directory) {
|
||||
continue;
|
||||
char* leaf = NULL;
|
||||
/* The parent walk is fd-relative and O_NOFOLLOW, so a symlink planted in a
|
||||
parent component can never redirect the utimensat outside the root. */
|
||||
parent component can never redirect the utimensat/chmod outside the
|
||||
root. */
|
||||
int parent_fd = file_open_secure_parent(dir_path, &leaf, false);
|
||||
if (parent_fd < 0) {
|
||||
free(dir_path);
|
||||
@@ -2493,8 +2520,8 @@ void dir_time_list_apply(const DirTimeList* list, const char* root_directory) {
|
||||
/* A dir-time entry only records metadata: the directory is (deliberately)
|
||||
not created from it, so an empty source directory (or one pruned by
|
||||
-m/--prune-empty-dirs) may well not exist here. Skip absent paths
|
||||
QUIETLY rather than warning for every one, and apply the times only to a
|
||||
real directory that does exist. AT_SYMLINK_NOFOLLOW keeps a same-named
|
||||
QUIETLY rather than warning for every one, and apply the metadata only to
|
||||
a real directory that does exist. AT_SYMLINK_NOFOLLOW keeps a same-named
|
||||
symlink from being followed; a pre-existing regular file/symlink is not a
|
||||
directory, so it is left completely untouched. */
|
||||
struct stat st;
|
||||
@@ -2504,18 +2531,56 @@ void dir_time_list_apply(const DirTimeList* list, const char* root_directory) {
|
||||
free(dir_path);
|
||||
continue;
|
||||
}
|
||||
struct timespec times[2] = {
|
||||
{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
|
||||
{.tv_sec = list->entries[i].mtime_sec, .tv_nsec = list->entries[i].mtime_nsec}};
|
||||
if (list->entries[i].atime_valid) {
|
||||
times[0].tv_sec = list->entries[i].atime_sec;
|
||||
times[0].tv_nsec = list->entries[i].atime_nsec;
|
||||
if (apply_times) {
|
||||
struct timespec times[2] = {
|
||||
{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
|
||||
{.tv_sec = list->entries[i].mtime_sec, .tv_nsec = list->entries[i].mtime_nsec}};
|
||||
if (config->preserve_atimes && list->entries[i].atime_valid) {
|
||||
times[0].tv_sec = list->entries[i].atime_sec;
|
||||
times[0].tv_nsec = list->entries[i].atime_nsec;
|
||||
}
|
||||
if (utimensat(parent_fd, leaf, times, AT_SYMLINK_NOFOLLOW) != 0) {
|
||||
char* escaped_path = output_escape(dir_path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "Failed to set directory timestamps on %s: %s",
|
||||
escaped_path ? escaped_path : "<allocation failed>", strerror(errno));
|
||||
free(escaped_path);
|
||||
}
|
||||
}
|
||||
if (utimensat(parent_fd, leaf, times, AT_SYMLINK_NOFOLLOW) != 0) {
|
||||
char* escaped_path = output_escape(dir_path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "Failed to set directory timestamps on %s: %s",
|
||||
escaped_path ? escaped_path : "<allocation failed>", strerror(errno));
|
||||
free(escaped_path);
|
||||
if (apply_mode) {
|
||||
mode_t dir_mode = list->entries[i].mode;
|
||||
bool mode_ready = true;
|
||||
if (config->chmod_spec && *config->chmod_spec &&
|
||||
!chmod_apply(dir_mode, config->chmod_spec, &dir_mode)) {
|
||||
char* escaped_path = output_escape(dir_path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "Failed to apply --chmod to directory %s",
|
||||
escaped_path ? escaped_path : "<allocation failed>");
|
||||
free(escaped_path);
|
||||
mode_ready = false;
|
||||
}
|
||||
if (mode_ready) {
|
||||
/* Route the directory mode through the SAME sanitization as the
|
||||
* regular-file policy: a client-supplied mode never grants group/other
|
||||
* write. Open the directory with O_DIRECTORY|O_NOFOLLOW (never
|
||||
* following a same-named symlink) and fchmod the fd, avoiding the
|
||||
* fchmodat(..., 0) TOCTOU/symlink-follow hole. */
|
||||
mode_t safe_mode =
|
||||
(dir_mode & 0777 & ~(S_IWGRP | S_IWOTH)) | (dir_mode & (S_ISGID | S_ISVTX));
|
||||
int dir_fd = openat(parent_fd, leaf, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if (dir_fd < 0) {
|
||||
char* escaped_path = output_escape(dir_path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "Failed to open directory %s to set its mode: %s",
|
||||
escaped_path ? escaped_path : "<allocation failed>", strerror(errno));
|
||||
free(escaped_path);
|
||||
} else {
|
||||
if (fchmod(dir_fd, safe_mode) != 0) {
|
||||
char* escaped_path = output_escape(dir_path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "Failed to set directory mode on %s: %s",
|
||||
escaped_path ? escaped_path : "<allocation failed>", strerror(errno));
|
||||
free(escaped_path);
|
||||
}
|
||||
close(dir_fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
|
||||
+15
-10
@@ -48,10 +48,12 @@ typedef struct {
|
||||
} DirTimeList;
|
||||
|
||||
/* Capture gate shared by the sender-side and receiver-side sinks: directory
|
||||
* metadata is accumulated only when --times/--metadata is in effect and
|
||||
* -O/--omit-dir-times does not suppress it. Kept here, next to the accumulator
|
||||
* it guards, so both call sites express the same condition. */
|
||||
bool dir_times_should_capture(const Config* config);
|
||||
* metadata is accumulated only when a directory attribute is requested
|
||||
* (-p/--perms for directory modes, or -t/--times for directory mtimes with
|
||||
* -O/--omit-dir-times not suppressing them) and metadata rides the wire. Kept
|
||||
* here, next to the accumulator it guards, so both call sites express the same
|
||||
* condition. */
|
||||
bool dir_metadata_should_capture(const Config* config);
|
||||
|
||||
void dir_time_list_init(DirTimeList* list);
|
||||
void dir_time_list_free(DirTimeList* list);
|
||||
@@ -59,12 +61,15 @@ void dir_time_list_free(DirTimeList* list);
|
||||
* allocation failure OR when the cumulative entry/byte caps would be exceeded
|
||||
* (the caller fails the transfer). */
|
||||
bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetadata* metadata);
|
||||
/* Apply every accumulated directory's mtime (and atime when captured) beneath
|
||||
* `root_directory`, confined fd-relative. Best-effort per entry: an absent
|
||||
* directory (an empty/pruned source dir that was deliberately not created) or a
|
||||
* non-directory at the path is skipped QUIETLY, an unreachable one with a
|
||||
* warning, and never fatal. */
|
||||
void dir_time_list_apply(const DirTimeList* list, const char* root_directory);
|
||||
/* Apply every accumulated directory's metadata beneath `root_directory`,
|
||||
* confined fd-relative. Times (mtime, plus atime when -U captured one) are
|
||||
* applied only when config->preserve_times && !config->omit_dir_times; the mode
|
||||
* (through --chmod when configured) is applied only when config->preserve_perms.
|
||||
* Best-effort per entry: an absent directory (an empty/pruned source dir that
|
||||
* was deliberately not created) or a non-directory at the path is skipped
|
||||
* QUIETLY, an unreachable one with a warning, and never fatal. */
|
||||
void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory,
|
||||
const Config* config);
|
||||
|
||||
/* A received delete-manifest frame: the keep-set (`keeps`, destination-relative
|
||||
paths the sender transferred/keeps) plus `protected`, destination-relative
|
||||
|
||||
+126
-76
@@ -36,6 +36,13 @@ typedef struct {
|
||||
bool copy_as_set;
|
||||
int32_t copy_as_uid;
|
||||
int32_t copy_as_gid;
|
||||
/* -o/--owner and -g/--group: preserve the source owner/group through the
|
||||
* normal name/identity resolution path. Split out of the former
|
||||
* use_metadata bundle; unlike --numeric-ids/--chown/--usermap/--groupmap/-a
|
||||
* these are a preserve-source request, not an arbitrary client-chosen owner,
|
||||
* so they are tracked separately from the explicit ownership gate. */
|
||||
bool preserve_owner;
|
||||
bool preserve_group;
|
||||
bool set;
|
||||
} IdentityActive;
|
||||
|
||||
@@ -57,6 +64,8 @@ static void identity_active_reset(void) {
|
||||
g_identity.copy_as_set = false;
|
||||
g_identity.copy_as_uid = 0;
|
||||
g_identity.copy_as_gid = 0;
|
||||
g_identity.preserve_owner = false;
|
||||
g_identity.preserve_group = false;
|
||||
g_identity.set = false;
|
||||
}
|
||||
|
||||
@@ -77,6 +86,8 @@ bool identity_set_active(const Config* config) {
|
||||
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;
|
||||
g_identity.preserve_owner = config->preserve_owner;
|
||||
g_identity.preserve_group = config->preserve_group;
|
||||
if (config->usermap_count > 0) {
|
||||
g_identity.usermap = calloc((size_t)config->usermap_count, sizeof(IdentityMap));
|
||||
if (!g_identity.usermap)
|
||||
@@ -95,14 +106,22 @@ bool identity_set_active(const Config* config) {
|
||||
}
|
||||
g_identity.set = true;
|
||||
/* A root receiver would honor any client-supplied ownership request (a
|
||||
--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");
|
||||
--usermap/--groupmap/--chown/--copy-as, or raw ids under --numeric-ids)
|
||||
ONLY when super-user activities are permitted. --no-super (or a daemon
|
||||
veto that forced SUPER_MODE_OFF) forbids the chown even for root, so do
|
||||
not claim the ownership will be honored in that case. */
|
||||
if (geteuid() == 0) {
|
||||
if (privilege_super_mode_permitted(g_identity.super_mode))
|
||||
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");
|
||||
else
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"identity mapping active and running as root, but super-user activities are "
|
||||
"disabled (--no-super): requested ownership will NOT be applied; 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 the kernel will
|
||||
refuse those confined attempts and each is skipped per entry. Warn exactly
|
||||
@@ -148,15 +167,47 @@ bool identity_active_enabled(void) {
|
||||
identity flag must never silently apply client-chosen ownership. */
|
||||
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.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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
bool identity_ownership_requested(const Config* config) {
|
||||
if (!config)
|
||||
return false;
|
||||
/* Every value that makes the receiver act on a client-chosen owner, plus an
|
||||
* explicit --super (super-user device-node activities). Pure config, so the
|
||||
* daemon gate can evaluate it before identity_set_active(). */
|
||||
/* General-awareness predicate: every value that makes the receiver act on a
|
||||
* client-chosen owner, plus an explicit --super (super-user device-node
|
||||
* activities) and the preserve-source -o/-g requests. Pure config, so callers
|
||||
* can evaluate it before identity_set_active(). The daemon module gate uses
|
||||
* the narrower identity_explicit_ownership_requested() below, which treats a
|
||||
* plain -o/-g/-a as a preserve-source request rather than arbitrary
|
||||
* client-chosen ownership. */
|
||||
return config->numeric_ids || config->chown_uid_set || config->chown_gid_set ||
|
||||
config->usermap_count > 0 || config->groupmap_count > 0 || config->copy_as_set ||
|
||||
config->preserve_owner || config->preserve_group || config->fake_super ||
|
||||
config->super_mode == SUPER_MODE_ON;
|
||||
}
|
||||
|
||||
bool identity_explicit_ownership_requested(const Config* config) {
|
||||
if (!config)
|
||||
return false;
|
||||
/* The narrow set the daemon gate refuses for a non-opted module: a request
|
||||
* that lets the CLIENT choose an arbitrary owner/group (rather than preserve
|
||||
* the source's own). Deliberately EXCLUDES preserve_owner/preserve_group so a
|
||||
* plain -a/-o/-g push is not refused; for those the gate instead forces
|
||||
* super-user ownership activity off (no chown happens) unless the module has
|
||||
* `client owner = yes`. */
|
||||
return config->numeric_ids || config->chown_uid_set || config->chown_gid_set ||
|
||||
config->usermap_count > 0 || config->groupmap_count > 0 || config->copy_as_set ||
|
||||
config->fake_super || config->super_mode == SUPER_MODE_ON;
|
||||
@@ -567,9 +618,6 @@ int identity_parse_copy_as(Config* config, const char* value) {
|
||||
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;
|
||||
ret = 0;
|
||||
|
||||
done:
|
||||
@@ -596,18 +644,13 @@ static bool identity_map_lookup(const IdentityMap* map, int count, int32_t sourc
|
||||
* paths. Returns false when no side is to be changed. */
|
||||
static bool identity_resolve_targets(const struct stat* st, int32_t source_uid, int32_t source_gid,
|
||||
uid_t* out_uid, gid_t* out_gid) {
|
||||
bool set_uid = false;
|
||||
bool set_gid = false;
|
||||
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;
|
||||
uid_t uid = (uid_t)g_identity.copy_as_uid;
|
||||
gid_t gid = (gid_t)g_identity.copy_as_gid;
|
||||
if (st->st_uid == uid && st->st_gid == gid)
|
||||
return false;
|
||||
*out_uid = uid;
|
||||
@@ -615,61 +658,68 @@ static bool identity_resolve_targets(const struct stat* st, int32_t source_uid,
|
||||
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;
|
||||
set_uid = true;
|
||||
} 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) {
|
||||
uid = (uid_t)source_uid;
|
||||
set_uid = true;
|
||||
} else {
|
||||
/* Best-effort name mapping against the receiver's own database: if the
|
||||
* transmitted (numeric) id resolves to a name present on this machine,
|
||||
* re-resolve it. On a shared-account host this is the identity operation;
|
||||
* when the id has no name here, the user side is left alone. */
|
||||
struct passwd* pw = getpwuid((uid_t)source_uid);
|
||||
if (pw) {
|
||||
const struct passwd* mapped = getpwnam(pw->pw_name);
|
||||
if (mapped) {
|
||||
uid = mapped->pw_uid;
|
||||
set_uid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (identity_map_lookup(g_identity.groupmap, g_identity.groupmap_count, source_gid, &target)) {
|
||||
gid = target == IDENTITY_CURRENT ? getegid() : (gid_t)target;
|
||||
set_gid = true;
|
||||
} 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) {
|
||||
gid = (gid_t)source_gid;
|
||||
set_gid = true;
|
||||
} else {
|
||||
struct group* gr = getgrgid((gid_t)source_gid);
|
||||
if (gr) {
|
||||
const struct group* mapped = getgrnam(gr->gr_name);
|
||||
if (mapped) {
|
||||
gid = mapped->gr_gid;
|
||||
set_gid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!set_uid && !set_gid)
|
||||
/* Each side is resolved independently: -o/-g and the explicit identity flags
|
||||
* 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;
|
||||
/* An unset side keeps the file's current id so the other side can change. */
|
||||
if (!set_uid)
|
||||
uid = st->st_uid;
|
||||
if (!set_gid)
|
||||
gid = st->st_gid;
|
||||
/* Only change ownership when the target differs (avoid needless syscalls and
|
||||
* any chance of clearing setuid/setgid on an already-correct entry). */
|
||||
if (st->st_uid == uid && st->st_gid == gid)
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Only change ownership when a requested side actually differs (avoid
|
||||
* needless syscalls and any chance of clearing setuid/setgid on an
|
||||
* already-correct entry). */
|
||||
bool changed = (owner_requested && uid != st->st_uid) || (group_requested && gid != st->st_gid);
|
||||
if (!changed)
|
||||
return false;
|
||||
*out_uid = uid;
|
||||
*out_gid = gid;
|
||||
|
||||
+27
-6
@@ -80,16 +80,37 @@ void identity_clear_active(void);
|
||||
* snapshot. Ownership stays OFF ("do not apply") for every transfer that
|
||||
* requests none of them, preserving FastSync's existing behavior. --super /
|
||||
* --no-super alone does NOT enable ownership; an explicit identity flag
|
||||
* (--numeric-ids / --chown / --usermap / --groupmap / --copy-as) is required. */
|
||||
* (--numeric-ids / --chown / --usermap / --groupmap / --copy-as) or a
|
||||
* preserve-source -o/--owner / -g/--group request is required. */
|
||||
bool identity_active_enabled(void);
|
||||
|
||||
/* Pure, config-only predicate: true when the client requested ANY
|
||||
* client-chosen ownership or super-user activity (--numeric-ids, --chown,
|
||||
* --usermap/--groupmap, --copy-as, --fake-super, or an explicit --super). Used
|
||||
* by the daemon module gate to decide whether a module's per-module opt-in is
|
||||
* required; it never reads the per-connection snapshot. */
|
||||
/* Per-side predicates over the ACTIVE per-connection snapshot (call
|
||||
* identity_set_active() first). They mirror the owner_requested /
|
||||
* group_requested conditions inside identity_resolve_targets() exactly, so
|
||||
* callers that must apply only one side (e.g. the --fake-super owner replay)
|
||||
* can pass (uid_t)-1 / (gid_t)-1 for the side that was NOT requested and leave
|
||||
* it untouched. The owner side is requested by --copy-as, --chown USER,
|
||||
* --numeric-ids, -o/--owner, or a non-empty --usermap; the group side by
|
||||
* --copy-as, --chown :GROUP, --numeric-ids, -g/--group, or a non-empty
|
||||
* --groupmap. */
|
||||
bool identity_owner_requested(void);
|
||||
bool identity_group_requested(void);
|
||||
|
||||
/* Pure, config-only predicate: true when the client requested ANY client-chosen
|
||||
* ownership or super-user activity (--numeric-ids, --chown, --usermap/--groupmap,
|
||||
* --copy-as, --fake-super, an explicit --super, or a preserve-source -o/-g).
|
||||
* General awareness only; the daemon module gate uses the narrower
|
||||
* identity_explicit_ownership_requested() below. Never reads the snapshot. */
|
||||
bool identity_ownership_requested(const Config* config);
|
||||
|
||||
/* Pure, config-only predicate for the narrow set that lets the CLIENT choose an
|
||||
* arbitrary owner/group: --numeric-ids, --chown, --usermap/--groupmap,
|
||||
* --copy-as, --fake-super, or an explicit --super. Deliberately EXCLUDES a
|
||||
* plain -o/--owner / -g/--group (or -a) preserve-source request, which the
|
||||
* daemon gate handles by forcing super-user ownership activity off rather than
|
||||
* refusing the whole transfer. Never reads the snapshot. */
|
||||
bool identity_explicit_ownership_requested(const Config* config);
|
||||
|
||||
/* Apply the negotiated ownership to an already-written file descriptor.
|
||||
* source_uid/source_gid are the transmitted numeric ids. Resolution order:
|
||||
* --copy-as (highest priority, forces both ids), then a matching
|
||||
|
||||
+104
-52
@@ -209,22 +209,55 @@ FileMetadata* metadata_receive(int file_descriptor, int* ok) {
|
||||
return m;
|
||||
}
|
||||
|
||||
static mode_t metadata_mode(const FileMetadata* metadata, mode_t current_mode,
|
||||
bool preserve_executability) {
|
||||
bool metadata_mode_for_policy(mode_t source_mode, mode_t current_mode, FileAttrPolicy policy,
|
||||
mode_t* out_mode) {
|
||||
const mode_t execute_bits = S_IXUSR | S_IXGRP | S_IXOTH;
|
||||
if (preserve_executability)
|
||||
return (current_mode & 0777 & ~execute_bits) | (metadata->mode & execute_bits);
|
||||
return metadata->mode & 0777 & ~(S_IWGRP | S_IWOTH);
|
||||
if (policy.perms) {
|
||||
/* Group/other write is never granted from a client-supplied mode. */
|
||||
*out_mode = source_mode & 0777 & ~(S_IWGRP | S_IWOTH);
|
||||
return true;
|
||||
}
|
||||
if (policy.executability) {
|
||||
/* -E/--executability (rsync 3.4 rule): do NOT copy the source's execute
|
||||
* bits per class. If the source is executable at all, derive the execute
|
||||
* bits from the DESTINATION's own read bits (so a class that can read may
|
||||
* execute); otherwise clear every execute bit. This runs on the
|
||||
* destination-derived base (pre-existing dest mode, or source&~umask for a
|
||||
* new file), and leaves special bits untouched. --perms wins when both are
|
||||
* set (handled above). */
|
||||
mode_t base = current_mode & 0777;
|
||||
if (source_mode & 0111)
|
||||
*out_mode = base | ((base & 0444) >> 2);
|
||||
else
|
||||
*out_mode = base & ~execute_bits;
|
||||
return true;
|
||||
}
|
||||
/* Neither requested: no source mode is applied at all. */
|
||||
return false;
|
||||
}
|
||||
|
||||
void file_restore_metadata(const char* path, const FileMetadata* metadata,
|
||||
bool preserve_executability) {
|
||||
FileAttrPolicy file_attr_policy_from_config(const Config* config) {
|
||||
FileAttrPolicy policy = {false, false, false, false};
|
||||
if (config) {
|
||||
policy.perms = config->preserve_perms;
|
||||
policy.times = config->preserve_times;
|
||||
policy.atimes = config->preserve_atimes;
|
||||
policy.executability = config->use_executability;
|
||||
}
|
||||
return policy;
|
||||
}
|
||||
|
||||
void file_restore_metadata(const char* path, const FileMetadata* metadata, FileAttrPolicy policy) {
|
||||
if (metadata == NULL)
|
||||
return;
|
||||
struct stat current;
|
||||
mode_t current_mode = stat(path, ¤t) == 0 ? current.st_mode : 0;
|
||||
mode_t safe_mode = metadata_mode(metadata, current_mode, preserve_executability);
|
||||
if (chmod(path, safe_mode) != 0) {
|
||||
bool apply_mode = false;
|
||||
mode_t safe_mode = 0;
|
||||
if (policy.perms || policy.executability) {
|
||||
struct stat current;
|
||||
mode_t current_mode = stat(path, ¤t) == 0 ? current.st_mode : 0;
|
||||
apply_mode = metadata_mode_for_policy(metadata->mode, current_mode, policy, &safe_mode);
|
||||
}
|
||||
if (apply_mode && chmod(path, safe_mode) != 0) {
|
||||
char* escaped_path = output_escape(path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "Failed to chmod %s: %s",
|
||||
escaped_path ? escaped_path : "<allocation failed>", strerror(errno));
|
||||
@@ -232,14 +265,23 @@ void file_restore_metadata(const char* path, const FileMetadata* metadata,
|
||||
}
|
||||
/* Never apply client-supplied ownership. The descriptor API below is the
|
||||
receiver write path; retain this legacy API only for compatibility. */
|
||||
struct timespec times[2];
|
||||
times[0].tv_sec = 0;
|
||||
times[0].tv_nsec = UTIME_OMIT;
|
||||
times[1].tv_sec = metadata->mtime_sec;
|
||||
times[1].tv_nsec = metadata->mtime_nsec;
|
||||
if (metadata->atime_valid) {
|
||||
times[0].tv_sec = metadata->atime_sec;
|
||||
times[0].tv_nsec = metadata->atime_nsec;
|
||||
if (policy.times || (policy.atimes && metadata->atime_valid)) {
|
||||
struct timespec times[2] = {{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
|
||||
{.tv_sec = 0, .tv_nsec = UTIME_OMIT}};
|
||||
if (policy.times) {
|
||||
times[1].tv_sec = metadata->mtime_sec;
|
||||
times[1].tv_nsec = metadata->mtime_nsec;
|
||||
}
|
||||
if (policy.atimes && metadata->atime_valid) {
|
||||
times[0].tv_sec = metadata->atime_sec;
|
||||
times[0].tv_nsec = metadata->atime_nsec;
|
||||
}
|
||||
if (utimensat(AT_FDCWD, path, times, 0) != 0) {
|
||||
char* escaped_path = output_escape(path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "Failed to set timestamps on %s: %s",
|
||||
escaped_path ? escaped_path : "<allocation failed>", strerror(errno));
|
||||
free(escaped_path);
|
||||
}
|
||||
}
|
||||
if (metadata->crtime_valid) {
|
||||
log_message(LOG_LEVEL_DEBUG,
|
||||
@@ -247,16 +289,10 @@ void file_restore_metadata(const char* path, const FileMetadata* metadata,
|
||||
"setter exists",
|
||||
(long long)metadata->crtime_sec, metadata->crtime_nsec, path);
|
||||
}
|
||||
if (utimensat(AT_FDCWD, path, times, 0) != 0) {
|
||||
char* escaped_path = output_escape(path, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING, "Failed to set timestamps on %s: %s",
|
||||
escaped_path ? escaped_path : "<allocation failed>", strerror(errno));
|
||||
free(escaped_path);
|
||||
}
|
||||
}
|
||||
|
||||
bool file_restore_symlink_metadata(const char* path, const FileMetadata* metadata,
|
||||
bool omit_link_times) {
|
||||
FileAttrPolicy policy, bool omit_link_times) {
|
||||
if (path == NULL || metadata == NULL)
|
||||
return !identity_copy_as_active();
|
||||
char* leaf = NULL;
|
||||
@@ -269,18 +305,25 @@ bool file_restore_symlink_metadata(const char* path, const FileMetadata* metadat
|
||||
best-effort. */
|
||||
bool owned = identity_apply_ownership_link(parent_fd, leaf, (int32_t)metadata->uid,
|
||||
(int32_t)metadata->gid);
|
||||
/* Symlink mode: not settable on Linux (fchmodat AT_SYMLINK_NOFOLLOW returns
|
||||
EOPNOTSUPP/ENOTSUP); attempt it for platforms that support it and quietly
|
||||
ignore the unsupported case so the transfer never fails over it. */
|
||||
mode_t link_mode = metadata->mode & 0777;
|
||||
if (fchmodat(parent_fd, leaf, link_mode, AT_SYMLINK_NOFOLLOW) != 0 && errno != EOPNOTSUPP &&
|
||||
errno != ENOTSUP && errno != ENOSYS) {
|
||||
log_message(LOG_LEVEL_DEBUG, "Could not set symlink mode on %s: %s", path, strerror(errno));
|
||||
/* Symlink mode: only when -p is in effect. It is not settable on Linux
|
||||
(fchmodat AT_SYMLINK_NOFOLLOW returns EOPNOTSUPP/ENOTSUP); attempt it for
|
||||
platforms that support it and quietly ignore the unsupported case so the
|
||||
transfer never fails over it. */
|
||||
if (policy.perms) {
|
||||
mode_t link_mode = metadata->mode & 0777 & ~(S_IWGRP | S_IWOTH);
|
||||
if (fchmodat(parent_fd, leaf, link_mode, AT_SYMLINK_NOFOLLOW) != 0 && errno != EOPNOTSUPP &&
|
||||
errno != ENOTSUP && errno != ENOSYS) {
|
||||
log_message(LOG_LEVEL_DEBUG, "Could not set symlink mode on %s: %s", path, strerror(errno));
|
||||
}
|
||||
}
|
||||
if (!omit_link_times) {
|
||||
if (!omit_link_times && (policy.times || (policy.atimes && metadata->atime_valid))) {
|
||||
struct timespec times[2] = {{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
|
||||
{.tv_sec = metadata->mtime_sec, .tv_nsec = metadata->mtime_nsec}};
|
||||
if (metadata->atime_valid) {
|
||||
{.tv_sec = 0, .tv_nsec = UTIME_OMIT}};
|
||||
if (policy.times) {
|
||||
times[1].tv_sec = metadata->mtime_sec;
|
||||
times[1].tv_nsec = metadata->mtime_nsec;
|
||||
}
|
||||
if (policy.atimes && metadata->atime_valid) {
|
||||
times[0].tv_sec = metadata->atime_sec;
|
||||
times[0].tv_nsec = metadata->atime_nsec;
|
||||
}
|
||||
@@ -296,19 +339,22 @@ bool file_restore_symlink_metadata(const char* path, const FileMetadata* metadat
|
||||
return owned;
|
||||
}
|
||||
|
||||
bool file_restore_metadata_fd(int fd, const FileMetadata* metadata, bool preserve_executability) {
|
||||
bool file_restore_metadata_fd(int fd, const FileMetadata* metadata, FileAttrPolicy policy) {
|
||||
if (fd < 0 || metadata == NULL)
|
||||
return metadata == NULL;
|
||||
bool ok = true;
|
||||
struct stat current;
|
||||
if (fstat(fd, ¤t) != 0)
|
||||
return false;
|
||||
mode_t safe_mode = metadata_mode(metadata, current.st_mode, preserve_executability);
|
||||
if (fchmod(fd, safe_mode) != 0)
|
||||
ok = false;
|
||||
if (policy.perms || policy.executability) {
|
||||
struct stat current;
|
||||
if (fstat(fd, ¤t) != 0)
|
||||
return false;
|
||||
mode_t safe_mode = 0;
|
||||
bool apply_mode = metadata_mode_for_policy(metadata->mode, current.st_mode, policy, &safe_mode);
|
||||
if (apply_mode && fchmod(fd, safe_mode) != 0)
|
||||
ok = false;
|
||||
}
|
||||
/* Client uid/gid values are deliberately not authoritative UNLESS the client
|
||||
explicitly opted in with an identity flag (--numeric-ids / --usermap /
|
||||
--groupmap / --chown). identity_apply_ownership is the controlled,
|
||||
--groupmap / --chown / -o/-g). identity_apply_ownership is the controlled,
|
||||
privilege-gated path: it consults the negotiated policy, resolves the
|
||||
target ids, and applies them via an fd-relative fchown() that is confined
|
||||
to the just-written file (EPERM/EACCES are logged, never fatal) -- EXCEPT
|
||||
@@ -319,12 +365,6 @@ bool file_restore_metadata_fd(int fd, const FileMetadata* metadata, bool preserv
|
||||
ownership. */
|
||||
if (!identity_apply_ownership(fd, (int32_t)metadata->uid, (int32_t)metadata->gid))
|
||||
ok = false;
|
||||
struct timespec times[2] = {{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
|
||||
{.tv_sec = metadata->mtime_sec, .tv_nsec = metadata->mtime_nsec}};
|
||||
if (metadata->atime_valid) {
|
||||
times[0].tv_sec = metadata->atime_sec;
|
||||
times[0].tv_nsec = metadata->atime_nsec;
|
||||
}
|
||||
/* --crtimes captures and transmits the source birth time, but there is no
|
||||
* portable way to set a birth time (utimensat can only set atime/mtime), so
|
||||
* the receiver deliberately does NOT apply it. This is explicit, honest
|
||||
@@ -335,7 +375,19 @@ bool file_restore_metadata_fd(int fd, const FileMetadata* metadata, bool preserv
|
||||
"crtime (birth time) %lld.%09ld transmitted but not applied: no portable setter",
|
||||
(long long)metadata->crtime_sec, metadata->crtime_nsec);
|
||||
}
|
||||
if (futimens(fd, times) != 0)
|
||||
ok = false;
|
||||
if (policy.times || (policy.atimes && metadata->atime_valid)) {
|
||||
struct timespec times[2] = {{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
|
||||
{.tv_sec = 0, .tv_nsec = UTIME_OMIT}};
|
||||
if (policy.times) {
|
||||
times[1].tv_sec = metadata->mtime_sec;
|
||||
times[1].tv_nsec = metadata->mtime_nsec;
|
||||
}
|
||||
if (policy.atimes && metadata->atime_valid) {
|
||||
times[0].tv_sec = metadata->atime_sec;
|
||||
times[0].tv_nsec = metadata->atime_nsec;
|
||||
}
|
||||
if (futimens(fd, times) != 0)
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
+20
-7
@@ -2,6 +2,7 @@
|
||||
#define METADATA_H
|
||||
|
||||
#include "file.h"
|
||||
#include "file_attr.h"
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
@@ -49,19 +50,31 @@ void metadata_to_buf(char** buf, const FileMetadata* m);
|
||||
FileMetadata* metadata_from_buf(const uint8_t* buf, size_t len);
|
||||
bool metadata_send(int file_descriptor, const FileMetadata* m);
|
||||
FileMetadata* metadata_receive(int file_descriptor, int* ok);
|
||||
void file_restore_metadata(const char* path, const FileMetadata* metadata,
|
||||
bool preserve_executability);
|
||||
bool file_restore_metadata_fd(int fd, const FileMetadata* metadata, bool preserve_executability);
|
||||
void file_restore_metadata(const char* path, const FileMetadata* metadata, FileAttrPolicy policy);
|
||||
bool file_restore_metadata_fd(int fd, const FileMetadata* metadata, FileAttrPolicy policy);
|
||||
|
||||
/* Shared mode-policy helper: the single source of truth for the receiver's
|
||||
* mode rule. Given a source mode and the destination's CURRENT mode, returns
|
||||
* true and stores the exact mode to apply in *out_mode when `policy` requests
|
||||
* a change, or false when it requests neither --perms nor --executability (the
|
||||
* caller then leaves the destination mode alone). --perms wins over -E; the
|
||||
* -E rule derives exec bits from the destination's read bits (rsync 3.4);
|
||||
* group/other write is never granted from a client-supplied mode. Shared by
|
||||
* file_restore_metadata_fd() and the --fake-super replay so the two cannot
|
||||
* diverge. */
|
||||
bool metadata_mode_for_policy(mode_t source_mode, mode_t current_mode, FileAttrPolicy policy,
|
||||
mode_t* out_mode);
|
||||
/* P7 Wave D: apply a SYMLINK's own metadata using no-follow primitives only
|
||||
* (utimensat/lchown/fchmodat with AT_SYMLINK_NOFOLLOW), confined fd-relative
|
||||
* under the authorized root. `omit_link_times` (-J/--omit-link-times)
|
||||
* suppresses the timestamps; the link's mode/ownership are still attempted
|
||||
* (ownership stays gated by the identity policy and by default is not applied).
|
||||
* under the authorized root. The link's mode is applied only when policy.perms;
|
||||
* policy.times (further suppressed by `omit_link_times` for -J) applies the
|
||||
* mtime with policy.atimes controlling the atime slot; ownership stays gated by
|
||||
* the identity policy and by default is not applied.
|
||||
* A null metadata or an unfollowable parent is a harmless no-op. Returns false
|
||||
* only when a REQUIRED --copy-as ownership application failed, so the caller can
|
||||
* report the entry as failed instead of claiming a wrong-owner success. */
|
||||
bool file_restore_symlink_metadata(const char* path, const FileMetadata* metadata,
|
||||
bool omit_link_times);
|
||||
FileAttrPolicy policy, bool omit_link_times);
|
||||
|
||||
/* Compare timestamps using rsync's whole-second modification window. */
|
||||
bool metadata_mtime_matches(time_t left_sec, long left_nsec, time_t right_sec, long right_nsec,
|
||||
|
||||
+47
-23
@@ -2,6 +2,7 @@
|
||||
#include "xattr.h"
|
||||
#include "identity.h"
|
||||
#include "log.h"
|
||||
#include "metadata.h"
|
||||
#include "protocol.h"
|
||||
#include "utils.h"
|
||||
#include "file_types.h"
|
||||
@@ -371,11 +372,15 @@ void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, int6
|
||||
* still applies mode/mtime where permitted.
|
||||
*
|
||||
* The OWNER leg additionally honors three policies:
|
||||
* - an explicit ownership identity policy must be active (numeric-ids /
|
||||
* chown / usermap / groupmap / copy-as). --fake-super on its own only
|
||||
* RECORDS the source owner; replaying that owner as a live chown without an
|
||||
* explicit ownership opt-in would be an un-gated client-chosen-ownership
|
||||
* primitive.
|
||||
* - an ownership identity policy must be active: the explicit flags
|
||||
* (--numeric-ids / --chown / --usermap / --groupmap / --copy-as) OR the
|
||||
* preserve-source -o/--owner / -g/--group requests. --fake-super on its own
|
||||
* only RECORDS the source owner; replaying that owner as a live chown
|
||||
* without an ownership opt-in would be an un-gated client-chosen-ownership
|
||||
* primitive. The owner and group sides are applied INDEPENDENTLY (through
|
||||
* identity_owner_requested()/identity_group_requested()), so a plain -o or
|
||||
* -g touches only the requested side and passes (uid_t)-1 / (gid_t)-1 for
|
||||
* the other.
|
||||
* - --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
|
||||
@@ -383,7 +388,7 @@ void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, int6
|
||||
* 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) {
|
||||
bool fake_super_restore_fd(int fd, FileAttrPolicy policy) {
|
||||
if (fd < 0)
|
||||
return false;
|
||||
char record[128];
|
||||
@@ -405,21 +410,40 @@ bool fake_super_restore_fd(int fd) {
|
||||
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 (identity_active_enabled() && 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
|
||||
uses (metadata_mode): group/other write bits are never granted, so a
|
||||
recorded source mode of 0666 restores as 0644 — identical to a non-fake-
|
||||
super --preserve run, never a privilege-granting regression. */
|
||||
if (fchmod(fd, (mode_t)(ul_mode & 0777U & ~(S_IWGRP | S_IWOTH))) != 0)
|
||||
log_message(LOG_LEVEL_WARNING, "--fake-super: could not restore mode on destination file: %s",
|
||||
strerror(errno));
|
||||
struct timespec times[2] = {{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
|
||||
{.tv_sec = (time_t)mtime_sec, .tv_nsec = mtime_nsec}};
|
||||
if (futimens(fd, times) != 0)
|
||||
log_message(LOG_LEVEL_WARNING, "--fake-super: could not restore mtime on destination file: %s",
|
||||
strerror(errno));
|
||||
if (identity_active_enabled() && privilege_super_permitted() && !identity_copy_as_active()) {
|
||||
/* Apply only the requested side(s): an unchosen side is passed as -1 so the
|
||||
* kernel leaves it exactly as-is. */
|
||||
uid_t owner = identity_owner_requested() ? (uid_t)ul_uid : (uid_t)-1;
|
||||
gid_t group = identity_group_requested() ? (gid_t)ul_gid : (gid_t)-1;
|
||||
if (fchown(fd, owner, group) != 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 only when the per-attribute policy asks for it, through the
|
||||
SAME shared helper the normal metadata path uses (metadata_mode_for_policy):
|
||||
group/other write bits are never granted, so a recorded source mode of 0666
|
||||
restores as 0644 — identical to a non-fake-super --preserve run, never a
|
||||
privilege-granting regression — and the -E rule derives exec bits from the
|
||||
destination's read bits exactly like file_restore_metadata_fd. */
|
||||
if (policy.perms || policy.executability) {
|
||||
struct stat cur;
|
||||
mode_t want = 0;
|
||||
if (fstat(fd, &cur) != 0) {
|
||||
log_message(LOG_LEVEL_WARNING, "--fake-super: could not read destination mode: %s",
|
||||
strerror(errno));
|
||||
} else if (metadata_mode_for_policy((mode_t)ul_mode, cur.st_mode, policy, &want)) {
|
||||
if (fchmod(fd, want) != 0)
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"--fake-super: could not restore mode on destination file: %s",
|
||||
strerror(errno));
|
||||
}
|
||||
}
|
||||
if (policy.times) {
|
||||
struct timespec times[2] = {{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
|
||||
{.tv_sec = (time_t)mtime_sec, .tv_nsec = mtime_nsec}};
|
||||
if (futimens(fd, times) != 0)
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"--fake-super: could not restore mtime on destination file: %s", strerror(errno));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -1,6 +1,7 @@
|
||||
#ifndef XATTR_H
|
||||
#define XATTR_H
|
||||
|
||||
#include "file_attr.h"
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
@@ -99,11 +100,13 @@ void fake_super_store_fd(int fd, uint32_t uid, uint32_t gid, uint32_t mode, int6
|
||||
* Best-effort: absence of the xattr or a malformed record is a silent no-op
|
||||
* that never fails the transfer. The OWNER leg is applied only when an explicit
|
||||
* ownership identity policy is active (numeric-ids/chown/usermap/groupmap/
|
||||
* copy-as), when super-user activities are permitted, and when --copy-as is not
|
||||
* authoritative; a non-root EPERM/EACCES is skipped silently, matching
|
||||
* FastSync's identity philosophy. The mode is sanitized exactly like the normal
|
||||
* metadata path (group/other write bits never granted). Returns true when the
|
||||
* xattr was present and parsed. */
|
||||
bool fake_super_restore_fd(int fd);
|
||||
* copy-as/-o/-g), when super-user activities are permitted, and when --copy-as
|
||||
* is not authoritative; a non-root EPERM/EACCES is skipped silently, matching
|
||||
* FastSync's identity philosophy. The MODE leg is applied only when
|
||||
* policy.perms||policy.executability and the MTIME leg only when policy.times,
|
||||
* so the fake-super replay cannot bypass the per-attribute split; the mode is
|
||||
* sanitized exactly like the normal metadata path (group/other write bits never
|
||||
* granted). Returns true when the xattr was present and parsed. */
|
||||
bool fake_super_restore_fd(int fd, FileAttrPolicy policy);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user