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:
2026-09-15 19:32:02 +02:00
parent b3f7cad4db
commit 34970b961c
35 changed files with 2523 additions and 485 deletions
+104 -54
View File
@@ -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. */
+7 -5
View File
@@ -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
View File
@@ -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");
+7 -5
View File
@@ -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);
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+38
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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, &current) == 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, &current) == 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, &current) != 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, &current) != 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
View File
@@ -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
View File
@@ -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
View File
@@ -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