Merge feat/ref-integration: SuperMode enum, dir-time gate dedup, parse_args/server_module_gate splits
CI / lint (push) Successful in 1m30s
CI / sanitizers (undefined) (push) Successful in 59s
CI / sanitizers (address) (push) Successful in 1m6s
CI / fuzz-build (push) Successful in 29s
CI / coverage (push) Successful in 49s
CI / valgrind (push) Successful in 2m9s
CI / build-and-test (push) Successful in 4m31s
CI / lint (push) Successful in 1m30s
CI / sanitizers (undefined) (push) Successful in 59s
CI / sanitizers (address) (push) Successful in 1m6s
CI / fuzz-build (push) Successful in 29s
CI / coverage (push) Successful in 49s
CI / valgrind (push) Successful in 2m9s
CI / build-and-test (push) Successful in 4m31s
This commit is contained in:
+987
-629
File diff suppressed because it is too large
Load Diff
@@ -353,7 +353,7 @@ static bool receiver_save_file(File* file, void* context_pointer) {
|
|||||||
metadata now and apply it at the end. -O/--omit-dir-times is honored by
|
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). */
|
dir_time_list_apply's caller (see receiver_send_success_frame). */
|
||||||
if (result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
if (result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
||||||
context->config->use_metadata && !context->config->omit_dir_times &&
|
dir_times_should_capture(context->config) &&
|
||||||
!dir_time_list_add(&context->dir_times, file->path, file->metadata)) {
|
!dir_time_list_add(&context->dir_times, file->path, file->metadata)) {
|
||||||
file_destroy(file);
|
file_destroy(file);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+166
-109
@@ -251,6 +251,155 @@ static bool configure_authorization(const char* root) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Discriminates the outcome of the A7 auth gate so the dispatcher can map it
|
||||||
|
* back to the config_receive_with_validate contract: accepted (including
|
||||||
|
* "module needs no auth"), a config-level refusal carrying an error string, or
|
||||||
|
* a handshake that already wrote its own terminal status frame. */
|
||||||
|
typedef enum {
|
||||||
|
MODULE_AUTH_ACCEPTED = 0,
|
||||||
|
MODULE_AUTH_REFUSED,
|
||||||
|
MODULE_AUTH_TERMINATED,
|
||||||
|
} ModuleAuthResult;
|
||||||
|
|
||||||
|
/* Looks up the daemon module selected by the client's config frame and rejects
|
||||||
|
* a `read only` one (every FastSync network transfer writes; there is no
|
||||||
|
* read-only wire operation yet). Returns the module, or NULL with *error set
|
||||||
|
* to the caller-facing rejection message. */
|
||||||
|
static const DaemonModule* module_gate_lookup_module(const Config* config, const char** error) {
|
||||||
|
const DaemonModule* module = daemon_conf_find_module(g_daemon_conf, config->module);
|
||||||
|
if (module == NULL) {
|
||||||
|
char* escaped_module = output_escape(config->module, config->eight_bit_output);
|
||||||
|
log_message(LOG_LEVEL_ERROR, "unknown daemon module '%s' requested",
|
||||||
|
escaped_module ? escaped_module : "<allocation failed>");
|
||||||
|
free(escaped_module);
|
||||||
|
*error = "requested daemon module does not exist";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (module->read_only) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "daemon module '%s' is read only; refusing write transfer",
|
||||||
|
config->module);
|
||||||
|
*error = "requested daemon module is read only";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-module client-chosen ownership / super-user policy (P7 Wave E hardening):
|
||||||
|
* a daemon module refuses EVERY ownership-affecting request (--numeric-ids,
|
||||||
|
* --chown, --usermap/--groupmap, --fake-super, --copy-as, explicit --super)
|
||||||
|
* unless the operator opted THIS module in with `client owner = yes`.
|
||||||
|
* Otherwise any client could force arbitrary ownership inside the module root.
|
||||||
|
* The ownership check is evaluated against the ORIGINAL config so an explicit
|
||||||
|
* --super is refused even when an operator --no-super veto already forced the
|
||||||
|
* effective copy to OFF (the veto must not silently convert a refusal into an
|
||||||
|
* accept); when no ownership flag is present, super-user DEVICE activities are
|
||||||
|
* forced off for this connection instead. Returns an error string on refusal,
|
||||||
|
* NULL on acceptance. */
|
||||||
|
static const char* module_gate_check_ownership(const Config* config, const DaemonModule* module,
|
||||||
|
ModuleGateContext* gate_ctx) {
|
||||||
|
if (module->client_owner)
|
||||||
|
return NULL;
|
||||||
|
/* Ownership: refuse the whole transfer up front (a clear failure). */
|
||||||
|
if (identity_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",
|
||||||
|
config->module);
|
||||||
|
return "client-chosen ownership is not permitted by this daemon module";
|
||||||
|
}
|
||||||
|
/* Super-user DEVICE activities (char/block mknod and --write-devices) are
|
||||||
|
permitted under the default AUTO mode, so without this override a root
|
||||||
|
daemon would still let a non-opted module create arbitrary device nodes
|
||||||
|
and write raw devices. Force them off for this connection: those entries
|
||||||
|
are skipped (never mknod'ed) while an ordinary `-a` push still succeeds
|
||||||
|
without device nodes, matching the operator's least-privilege choice.
|
||||||
|
The operator-level --no-super veto is already folded into this. */
|
||||||
|
if (gate_ctx)
|
||||||
|
gate_ctx->super_mode_override = SUPER_MODE_OFF;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A7 auth gate: runs the SCRAM challenge/response for an auth-required module
|
||||||
|
* BEFORE the module root is installed and before any data moves. Returns
|
||||||
|
* MODULE_AUTH_ACCEPTED when the module needs no auth or the handshake succeeds,
|
||||||
|
* MODULE_AUTH_REFUSED with *error set on a config-level rejection, or
|
||||||
|
* MODULE_AUTH_TERMINATED when the handshake already wrote a terminal status. */
|
||||||
|
static ModuleAuthResult module_gate_authenticate(const Config* config, const DaemonModule* module,
|
||||||
|
ModuleGateContext* gate_ctx, const char** error) {
|
||||||
|
if (module->auth_user_count == 0)
|
||||||
|
return MODULE_AUTH_ACCEPTED;
|
||||||
|
/* Fail closed: no store -> refuse (server misconfiguration, STATUS_ERROR). */
|
||||||
|
if (g_credentials == NULL) {
|
||||||
|
log_message(LOG_LEVEL_ERROR,
|
||||||
|
"daemon module '%s' requires authentication but no credential store is "
|
||||||
|
"configured (--password-file/--early-input); refusing",
|
||||||
|
config->module);
|
||||||
|
*error = "requested daemon module requires authentication and no credential "
|
||||||
|
"store is configured";
|
||||||
|
return MODULE_AUTH_REFUSED;
|
||||||
|
}
|
||||||
|
/* Transport policy (A7-3/S1): an auth-required module only accepts
|
||||||
|
* credentials over (a) an encrypted, verified TLS connection whose client
|
||||||
|
* certificate matches --client-cn, or (b) an actual PLAINTEXT connection
|
||||||
|
* from a loopback peer that the operator explicitly opted into with
|
||||||
|
* --allow-unauthenticated. A remote plaintext peer, an un-flagged loopback
|
||||||
|
* plaintext peer, and a loopback TLS peer whose certificate does not match
|
||||||
|
* --client-cn are all refused HERE, before the challenge is sent, so an
|
||||||
|
* unverified client never receives a nonce: the loopback allowance requires
|
||||||
|
* !gate_ctx->ssl, so --tls + --allow-unauthenticated can never be used to
|
||||||
|
* bypass the client-CN check. The operator flag never permits REMOTE
|
||||||
|
* plaintext auth: remote peers still require verified TLS regardless. */
|
||||||
|
bool tls_ok = gate_ctx && gate_ctx->ssl && SSL_get_verify_result(gate_ctx->ssl) == X509_V_OK &&
|
||||||
|
tls_client_identity_allowed(gate_ctx->ssl);
|
||||||
|
bool local_ok = allow_unauthenticated && gate_ctx && !gate_ctx->ssl && gate_ctx->fd >= 0 &&
|
||||||
|
utils_fd_peer_is_local(gate_ctx->fd);
|
||||||
|
if (!tls_ok && !local_ok) {
|
||||||
|
log_message(LOG_LEVEL_ERROR,
|
||||||
|
"daemon module '%s' requires authentication over an encrypted, verified TLS "
|
||||||
|
"connection (or an opted-in loopback plaintext transport); refusing",
|
||||||
|
config->module);
|
||||||
|
*error = "daemon module requires authentication over an encrypted, verified TLS "
|
||||||
|
"connection";
|
||||||
|
return MODULE_AUTH_REFUSED;
|
||||||
|
}
|
||||||
|
/* Belt-and-braces: the transport policy above already guarantees a context
|
||||||
|
* with a usable socket (verified TLS implies a live SSL object and loopback
|
||||||
|
* allowance requires gate_ctx->fd >= 0), so this is unreachable today; keep
|
||||||
|
* the guard so the handshake can never be driven over an invalid fd. */
|
||||||
|
if (!gate_ctx || gate_ctx->fd < 0) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "daemon module '%s': no auth transport available", config->module);
|
||||||
|
*error = "authentication failed for the requested daemon module";
|
||||||
|
return MODULE_AUTH_REFUSED;
|
||||||
|
}
|
||||||
|
/* The handshake writes exactly one terminal status on failure and signals so
|
||||||
|
* via MODULE_AUTH_TERMINATED; the username may be logged (never the password
|
||||||
|
* or any derived proof). */
|
||||||
|
if (!server_auth_handshake(gate_ctx->fd, config, module)) {
|
||||||
|
char* escaped_user =
|
||||||
|
config->auth_user ? output_escape(config->auth_user, config->eight_bit_output) : NULL;
|
||||||
|
log_message(LOG_LEVEL_ERROR, "daemon module '%s': authentication failed for user '%s'",
|
||||||
|
config->module, escaped_user ? escaped_user : "(none)");
|
||||||
|
free(escaped_user);
|
||||||
|
return MODULE_AUTH_TERMINATED;
|
||||||
|
}
|
||||||
|
char* escaped_user = output_escape(config->auth_user, config->eight_bit_output);
|
||||||
|
log_message(LOG_LEVEL_INFO, "daemon module '%s': user '%s' authenticated", config->module,
|
||||||
|
escaped_user ? escaped_user : "<allocation failed>");
|
||||||
|
free(escaped_user);
|
||||||
|
return MODULE_AUTH_ACCEPTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Installs the module's configured path as the connection's authorized root.
|
||||||
|
* Returns an error string when the root is unusable, NULL on success. */
|
||||||
|
static const char* module_gate_install_root(const Config* config, const DaemonModule* module) {
|
||||||
|
if (!configure_authorization(module->path)) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "daemon module '%s' path '%s' is not usable", config->module,
|
||||||
|
module->path ? module->path : "(null)");
|
||||||
|
return "requested daemon module root is not usable";
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* Config-frame gate (runs inside config_receive_with_validate, BEFORE the
|
/* Config-frame gate (runs inside config_receive_with_validate, BEFORE the
|
||||||
* STATUS_OK ack, so a rejected connection is refused at the config handshake
|
* STATUS_OK ack, so a rejected connection is refused at the config handshake
|
||||||
* and no file data is ever exchanged).
|
* and no file data is ever exchanged).
|
||||||
@@ -324,115 +473,23 @@ static const char* server_module_gate(const Config* config, void* context) {
|
|||||||
return "daemon connection did not select a module (expected a "
|
return "daemon connection did not select a module (expected a "
|
||||||
"host::module/path destination)";
|
"host::module/path destination)";
|
||||||
|
|
||||||
const DaemonModule* module = daemon_conf_find_module(g_daemon_conf, config->module);
|
const char* error = NULL;
|
||||||
if (module == NULL) {
|
const DaemonModule* module = module_gate_lookup_module(config, &error);
|
||||||
char* escaped_module = output_escape(config->module, config->eight_bit_output);
|
if (!module)
|
||||||
log_message(LOG_LEVEL_ERROR, "unknown daemon module '%s' requested",
|
return error;
|
||||||
escaped_module ? escaped_module : "<allocation failed>");
|
error = module_gate_check_ownership(config, module, gate_ctx);
|
||||||
free(escaped_module);
|
if (error)
|
||||||
return "requested daemon module does not exist";
|
return error;
|
||||||
|
switch (module_gate_authenticate(config, module, gate_ctx, &error)) {
|
||||||
|
case MODULE_AUTH_REFUSED:
|
||||||
|
return error;
|
||||||
|
case MODULE_AUTH_TERMINATED:
|
||||||
|
return CONFIG_VALIDATE_ALREADY_TERMINATED;
|
||||||
|
case MODULE_AUTH_ACCEPTED:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (module->read_only) {
|
/* accepted; the authorized root is now the module's path */
|
||||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s' is read only; refusing write transfer",
|
return module_gate_install_root(config, module);
|
||||||
config->module);
|
|
||||||
return "requested daemon module is read only";
|
|
||||||
}
|
|
||||||
/* Client-chosen ownership / super-user policy (P7 Wave E hardening): a daemon
|
|
||||||
module refuses EVERY ownership-affecting request (--numeric-ids, --chown,
|
|
||||||
--usermap/--groupmap, --fake-super, --copy-as, explicit --super) unless the
|
|
||||||
operator opted THIS module in with `client owner = yes`. Otherwise any
|
|
||||||
client could force arbitrary ownership inside the module root. The
|
|
||||||
standalone/SSH server has a single operator-authorized root and keeps
|
|
||||||
honoring these. */
|
|
||||||
if (!module->client_owner) {
|
|
||||||
/* Ownership: refuse the whole transfer up front (a clear failure).
|
|
||||||
Evaluated against the ORIGINAL config so an explicit --super is refused
|
|
||||||
even when an operator --no-super veto already forced the effective copy
|
|
||||||
to OFF (the veto must not silently convert a refusal into an accept). */
|
|
||||||
if (identity_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",
|
|
||||||
config->module);
|
|
||||||
return "client-chosen ownership is not permitted by this daemon module";
|
|
||||||
}
|
|
||||||
/* Super-user DEVICE activities (char/block mknod and --write-devices) are
|
|
||||||
permitted under the default AUTO mode, so without this override a root
|
|
||||||
daemon would still let a non-opted module create arbitrary device nodes
|
|
||||||
and write raw devices. Force them off for this connection: those entries
|
|
||||||
are skipped (never mknod'ed) while an ordinary `-a` push still succeeds
|
|
||||||
without device nodes, matching the operator's least-privilege choice.
|
|
||||||
The operator-level --no-super veto is already folded into this. */
|
|
||||||
if (gate_ctx)
|
|
||||||
gate_ctx->super_mode_override = SUPER_MODE_OFF;
|
|
||||||
}
|
|
||||||
if (module->auth_user_count > 0) {
|
|
||||||
/* Auth-required module (A7, protocol 2.19.0): run the SCRAM challenge/
|
|
||||||
* response BEFORE the module root is installed and before any data moves.
|
|
||||||
* Fail closed: no store -> refuse (server misconfiguration, STATUS_ERROR);
|
|
||||||
* a handshake that fails before the success response writes exactly one
|
|
||||||
* STATUS_AUTH_FAILED before signalling ALREADY_TERMINATED (a failure while
|
|
||||||
* writing the success signature instead just drops the broken connection).
|
|
||||||
* The username may be logged (never the password or any derived proof). */
|
|
||||||
if (g_credentials == NULL) {
|
|
||||||
log_message(LOG_LEVEL_ERROR,
|
|
||||||
"daemon module '%s' requires authentication but no credential store is "
|
|
||||||
"configured (--password-file/--early-input); refusing",
|
|
||||||
config->module);
|
|
||||||
return "requested daemon module requires authentication and no credential "
|
|
||||||
"store is configured";
|
|
||||||
}
|
|
||||||
/* Transport policy (A7-3/S1): an auth-required module only accepts
|
|
||||||
* credentials over (a) an encrypted, verified TLS connection whose client
|
|
||||||
* certificate matches --client-cn, or (b) an actual PLAINTEXT connection
|
|
||||||
* from a loopback peer that the operator explicitly opted into with
|
|
||||||
* --allow-unauthenticated. A remote plaintext peer, an un-flagged loopback
|
|
||||||
* plaintext peer, and a loopback TLS peer whose certificate does not match
|
|
||||||
* --client-cn are all refused HERE, before the challenge is sent, so an
|
|
||||||
* unverified client never receives a nonce: the loopback allowance requires
|
|
||||||
* !gate_ctx->ssl, so --tls + --allow-unauthenticated can never be used to
|
|
||||||
* bypass the client-CN check. The operator flag never permits REMOTE
|
|
||||||
* plaintext auth: remote peers still require verified TLS regardless. */
|
|
||||||
bool tls_ok = gate_ctx && gate_ctx->ssl && SSL_get_verify_result(gate_ctx->ssl) == X509_V_OK &&
|
|
||||||
tls_client_identity_allowed(gate_ctx->ssl);
|
|
||||||
bool local_ok = allow_unauthenticated && gate_ctx && !gate_ctx->ssl && gate_ctx->fd >= 0 &&
|
|
||||||
utils_fd_peer_is_local(gate_ctx->fd);
|
|
||||||
if (!tls_ok && !local_ok) {
|
|
||||||
log_message(LOG_LEVEL_ERROR,
|
|
||||||
"daemon module '%s' requires authentication over an encrypted, verified TLS "
|
|
||||||
"connection (or an opted-in loopback plaintext transport); refusing",
|
|
||||||
config->module);
|
|
||||||
return "daemon module requires authentication over an encrypted, verified TLS "
|
|
||||||
"connection";
|
|
||||||
}
|
|
||||||
/* Belt-and-braces: the transport policy above already guarantees a context
|
|
||||||
* with a usable socket (verified TLS implies a live SSL object and loopback
|
|
||||||
* allowance requires gate_ctx->fd >= 0), so this is unreachable today; keep
|
|
||||||
* the guard so the handshake can never be driven over an invalid fd. */
|
|
||||||
if (!gate_ctx || gate_ctx->fd < 0) {
|
|
||||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s': no auth transport available",
|
|
||||||
config->module);
|
|
||||||
return "authentication failed for the requested daemon module";
|
|
||||||
}
|
|
||||||
if (!server_auth_handshake(gate_ctx->fd, config, module)) {
|
|
||||||
char* escaped_user =
|
|
||||||
config->auth_user ? output_escape(config->auth_user, config->eight_bit_output) : NULL;
|
|
||||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s': authentication failed for user '%s'",
|
|
||||||
config->module, escaped_user ? escaped_user : "(none)");
|
|
||||||
free(escaped_user);
|
|
||||||
return CONFIG_VALIDATE_ALREADY_TERMINATED;
|
|
||||||
}
|
|
||||||
char* escaped_user = output_escape(config->auth_user, config->eight_bit_output);
|
|
||||||
log_message(LOG_LEVEL_INFO, "daemon module '%s': user '%s' authenticated", config->module,
|
|
||||||
escaped_user ? escaped_user : "<allocation failed>");
|
|
||||||
free(escaped_user);
|
|
||||||
}
|
|
||||||
if (!configure_authorization(module->path)) {
|
|
||||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s' path '%s' is not usable", config->module,
|
|
||||||
module->path ? module->path : "(null)");
|
|
||||||
return "requested daemon module root is not usable";
|
|
||||||
}
|
|
||||||
return NULL; /* accepted; authorized root is now the module's path */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void handler(int file_descriptor) {
|
void handler(int file_descriptor) {
|
||||||
@@ -458,7 +515,7 @@ void handler(int file_descriptor) {
|
|||||||
* device-node creation) sees SUPER_MODE_OFF. The gate never mutated the
|
* device-node creation) sees SUPER_MODE_OFF. The gate never mutated the
|
||||||
* received config. */
|
* received config. */
|
||||||
if (gate_ctx.super_mode_override != -1)
|
if (gate_ctx.super_mode_override != -1)
|
||||||
config->super_mode = gate_ctx.super_mode_override;
|
config->super_mode = (SuperMode)gate_ctx.super_mode_override;
|
||||||
protocol_set_8_bit_output(config->eight_bit_output);
|
protocol_set_8_bit_output(config->eight_bit_output);
|
||||||
if (!authorized_root) {
|
if (!authorized_root) {
|
||||||
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
||||||
|
|||||||
+2
-2
@@ -1293,14 +1293,14 @@ static bool receive_iconv_spec(int fd, Config* c) {
|
|||||||
* validated to the SUPER_MODE_AUTO..SUPER_MODE_OFF range (also re-checked by
|
* validated to the SUPER_MODE_AUTO..SUPER_MODE_OFF range (also re-checked by
|
||||||
* validate_received_config). */
|
* validate_received_config). */
|
||||||
static bool send_privilege_options(int fd, const Config* c) {
|
static bool send_privilege_options(int fd, const Config* c) {
|
||||||
return send_int(fd, c->super_mode);
|
return send_int(fd, (int)c->super_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_privilege_options(int fd, Config* c) {
|
static bool receive_privilege_options(int fd, Config* c) {
|
||||||
int mode;
|
int mode;
|
||||||
if (!receive_int(fd, &mode) || mode < SUPER_MODE_AUTO || mode > SUPER_MODE_OFF)
|
if (!receive_int(fd, &mode) || mode < SUPER_MODE_AUTO || mode > SUPER_MODE_OFF)
|
||||||
return false;
|
return false;
|
||||||
c->super_mode = mode;
|
c->super_mode = (SuperMode)mode;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-10
@@ -68,6 +68,13 @@ typedef struct {
|
|||||||
int value; /* 0/1 for booleans, byte count for SO_RCVBUF/SO_SNDBUF */
|
int value; /* 0/1 for booleans, byte count for SO_RCVBUF/SO_SNDBUF */
|
||||||
} SockOptEntry;
|
} SockOptEntry;
|
||||||
|
|
||||||
|
/* --super / --no-super tri-state (Config->super_mode). AUTO (default) and ON
|
||||||
|
* both permit a confined super-user attempt (AUTO preserves FastSync's
|
||||||
|
* historical best-effort behavior; an unprivileged attempt is refused by the
|
||||||
|
* kernel and skipped per entry); OFF forbids the attempt even for root. See
|
||||||
|
* privilege_super_mode_permitted() in identity.h. */
|
||||||
|
typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF = 2 } SuperMode;
|
||||||
|
|
||||||
typedef struct Config {
|
typedef struct Config {
|
||||||
char* version;
|
char* version;
|
||||||
char* send_directory;
|
char* send_directory;
|
||||||
@@ -416,7 +423,7 @@ typedef struct Config {
|
|||||||
* as a trailing int so the receiver can enforce the policy. See
|
* 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_ownership_requested() in
|
||||||
* identity.h. */
|
* identity.h. */
|
||||||
int super_mode;
|
SuperMode super_mode;
|
||||||
|
|
||||||
// Receiver-side runtime staging registry for --delay-updates. Never sent
|
// Receiver-side runtime staging registry for --delay-updates. Never sent
|
||||||
// over the wire and never set on the sender side.
|
// over the wire and never set on the sender side.
|
||||||
@@ -644,15 +651,6 @@ typedef struct Config {
|
|||||||
#define IDENTITY_CURRENT (-1)
|
#define IDENTITY_CURRENT (-1)
|
||||||
#define MAX_IDENTITY_MAP 128
|
#define MAX_IDENTITY_MAP 128
|
||||||
|
|
||||||
/* --super / --no-super tri-state (Config->super_mode). AUTO (default) and ON
|
|
||||||
* both permit a confined super-user attempt (AUTO preserves FastSync's
|
|
||||||
* historical best-effort behavior; an unprivileged attempt is refused by the
|
|
||||||
* kernel and skipped per entry); OFF forbids the attempt even for root. See
|
|
||||||
* privilege_super_mode_permitted() in identity.h. */
|
|
||||||
#define SUPER_MODE_AUTO 0
|
|
||||||
#define SUPER_MODE_ON 1
|
|
||||||
#define SUPER_MODE_OFF 2
|
|
||||||
|
|
||||||
Config* config_create(void);
|
Config* config_create(void);
|
||||||
void config_delete(Config* config);
|
void config_delete(Config* config);
|
||||||
|
|
||||||
|
|||||||
@@ -2236,6 +2236,10 @@ File* file_receive(const Config* config, int file_descriptor) {
|
|||||||
|
|
||||||
/* ---- P7 Wave D: deferred directory times ---- */
|
/* ---- P7 Wave D: deferred directory times ---- */
|
||||||
|
|
||||||
|
bool dir_times_should_capture(const Config* config) {
|
||||||
|
return config->use_metadata && !config->omit_dir_times;
|
||||||
|
}
|
||||||
|
|
||||||
void dir_time_list_init(DirTimeList* list) {
|
void dir_time_list_init(DirTimeList* list) {
|
||||||
if (!list)
|
if (!list)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ typedef struct {
|
|||||||
size_t capacity;
|
size_t capacity;
|
||||||
} DirTimeList;
|
} 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);
|
||||||
|
|
||||||
void dir_time_list_init(DirTimeList* list);
|
void dir_time_list_init(DirTimeList* list);
|
||||||
void dir_time_list_free(DirTimeList* list);
|
void dir_time_list_free(DirTimeList* list);
|
||||||
/* Deep-copy one directory's path + metadata into the list. Returns false on
|
/* Deep-copy one directory's path + metadata into the list. Returns false on
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ typedef struct {
|
|||||||
/* --super / --no-super tri-state (SUPER_MODE_AUTO when unset). Snapshotted
|
/* --super / --no-super tri-state (SUPER_MODE_AUTO when unset). Snapshotted
|
||||||
* per connection so privilege_super_permitted() can gate super-user
|
* per connection so privilege_super_permitted() can gate super-user
|
||||||
* activities without a Config argument. */
|
* activities without a Config argument. */
|
||||||
int super_mode;
|
SuperMode super_mode;
|
||||||
/* --copy-as=USER[:GROUP]: snapshotted so the ownership resolver can force the
|
/* --copy-as=USER[:GROUP]: snapshotted so the ownership resolver can force the
|
||||||
* target ids without a Config argument. */
|
* target ids without a Config argument. */
|
||||||
bool copy_as_set;
|
bool copy_as_set;
|
||||||
@@ -128,7 +128,7 @@ bool privilege_super_permitted(void) {
|
|||||||
return privilege_super_mode_permitted(g_identity.super_mode);
|
return privilege_super_mode_permitted(g_identity.super_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool privilege_super_mode_permitted(int mode) {
|
bool privilege_super_mode_permitted(SuperMode mode) {
|
||||||
/* AUTO and ON both attempt the confined operation; OFF forbids it even for a
|
/* AUTO and ON both attempt the confined operation; OFF forbids it even for a
|
||||||
* root receiver. AUTO is the historical FastSync behavior (always attempt
|
* root receiver. AUTO is the historical FastSync behavior (always attempt
|
||||||
* and let the kernel refuse an unprivileged call, which the caller skips), so
|
* and let the kernel refuse an unprivileged call, which the caller skips), so
|
||||||
|
|||||||
@@ -128,6 +128,6 @@ bool identity_wire_valid(const Config* config);
|
|||||||
* best-effort behavior where an unprivileged attempt is refused by the kernel
|
* best-effort behavior where an unprivileged attempt is refused by the kernel
|
||||||
* and skipped. Neither EVER elevates privileges. */
|
* and skipped. Neither EVER elevates privileges. */
|
||||||
bool privilege_super_permitted(void);
|
bool privilege_super_permitted(void);
|
||||||
bool privilege_super_mode_permitted(int mode);
|
bool privilege_super_mode_permitted(SuperMode mode);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "data.h"
|
#include "data.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
|
#include "file_receive.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "queue.h"
|
#include "queue.h"
|
||||||
@@ -331,7 +332,7 @@ int write_thread(void* pipeline_context) {
|
|||||||
write would clobber them); accumulate the metadata here and let the
|
write would clobber them); accumulate the metadata here and let the
|
||||||
caller apply it once every writer has drained. */
|
caller apply it once every writer has drained. */
|
||||||
if (result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
if (result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
||||||
context->config->use_metadata && !context->config->omit_dir_times &&
|
dir_times_should_capture(context->config) &&
|
||||||
!dir_time_list_add(&context->dir_times, file->path, file->metadata)) {
|
!dir_time_list_add(&context->dir_times, file->path, file->metadata)) {
|
||||||
file_destroy(file);
|
file_destroy(file);
|
||||||
pipeline_context_receiver_note_bytes_released(context, file_bytes);
|
pipeline_context_receiver_note_bytes_released(context, file_bytes);
|
||||||
|
|||||||
+1
-1
@@ -1672,7 +1672,7 @@ static void test_config_receive_rejects_invalid_iconv_spec() {
|
|||||||
static void test_config_super_mode_wire_roundtrip() {
|
static void test_config_super_mode_wire_roundtrip() {
|
||||||
if (is_running_under_valgrind())
|
if (is_running_under_valgrind())
|
||||||
return;
|
return;
|
||||||
int modes[] = {SUPER_MODE_AUTO, SUPER_MODE_ON, SUPER_MODE_OFF};
|
SuperMode modes[] = {SUPER_MODE_AUTO, SUPER_MODE_ON, SUPER_MODE_OFF};
|
||||||
for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) {
|
for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) {
|
||||||
int p[2];
|
int p[2];
|
||||||
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user