Merge branch 'feat/p8h-core' into feat/p8-integration

This commit is contained in:
2026-09-12 15:22:00 +02:00
8 changed files with 207 additions and 106 deletions
+30 -9
View File
@@ -350,7 +350,10 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
}
if (is_sock) {
/* No standard filesystem call recreates a socket; best-effort unsupported. */
log_message(LOG_LEVEL_WARNING, "socket not recreated: %s (unsupported; skipped)", file->path);
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING, "socket not recreated: %s (unsupported; skipped)",
escaped_path ? escaped_path : "<allocation failed>");
free(escaped_path);
return FILE_SAVE_SKIPPED;
}
if (is_char || is_blk) {
@@ -363,9 +366,11 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
so the policy does not depend on a prior identity_set_active(). Pure
FIFO creation is unprivileged and deliberately NOT gated here. */
if (!privilege_super_mode_permitted(config->super_mode)) {
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING,
"skipping %s: super-user device-node creation is not permitted on this receiver",
file->path);
escaped_path ? escaped_path : "<allocation failed>");
free(escaped_path);
return FILE_SAVE_SKIPPED;
}
} else if (is_fifo) {
@@ -438,18 +443,26 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
free(destination);
return FILE_SAVE_SKIPPED;
}
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING, "refusing to replace existing entry with %s: %s (skipped)",
is_fifo ? "FIFO" : "device", file->path);
is_fifo ? "FIFO" : "device", escaped_path ? escaped_path : "<allocation failed>");
free(escaped_path);
} else if (errno == EPERM || errno == EACCES) {
/* Missing CAP_MKNOD / parent write permission: the environment cannot
create the node, so skip instead of failing the whole run. */
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING,
"skipping %s: cannot create %s node (%s)\n"
" --devices/--specials node creation needs privilege (CAP_MKNOD)",
file->path, is_fifo ? "FIFO" : "device", strerror(errno));
escaped_path ? escaped_path : "<allocation failed>", is_fifo ? "FIFO" : "device",
strerror(errno));
free(escaped_path);
} else {
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING, "failed to create %s %s: %s (skipped)",
is_fifo ? "FIFO" : "device", file->path, strerror(errno));
is_fifo ? "FIFO" : "device", escaped_path ? escaped_path : "<allocation failed>",
strerror(errno));
free(escaped_path);
}
close(parent_fd);
free(leaf);
@@ -512,22 +525,28 @@ static FileSaveResult file_save_write_device(const char* root_directory, const F
close(parent_fd);
if (fd < 0) {
free(destination);
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
const char* shown_path = escaped_path ? escaped_path : "<allocation failed>";
if (saved_errno == ENXIO || saved_errno == EAGAIN) {
/* A FIFO with no reader / an unreadable special: skip like every other
unusable write-devices target instead of blocking or failing. */
log_message(LOG_LEVEL_WARNING, "write-devices: %s not writable (%s); skipped", file->path,
log_message(LOG_LEVEL_WARNING, "write-devices: %s not writable (%s); skipped", shown_path,
strerror(saved_errno));
} else {
log_message(LOG_LEVEL_WARNING, "write-devices: cannot open %s (%s); skipped", file->path,
log_message(LOG_LEVEL_WARNING, "write-devices: cannot open %s (%s); skipped", shown_path,
strerror(saved_errno));
}
free(escaped_path);
return FILE_SAVE_SKIPPED;
}
struct stat st;
if (fstat(fd, &st) != 0 || !(S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))) {
close(fd);
free(destination);
log_message(LOG_LEVEL_WARNING, "write-devices: %s is not a device node; skipped", file->path);
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING, "write-devices: %s is not a device node; skipped",
escaped_path ? escaped_path : "<allocation failed>");
free(escaped_path);
return FILE_SAVE_SKIPPED;
}
bool ok = true;
@@ -596,10 +615,12 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
open below keeps its own confinement and best-effort skip semantics). */
if (config && config->write_devices) {
if (!privilege_super_mode_permitted(config->super_mode)) {
char* escaped_path = output_escape(file->path, log_get_8_bit_output());
log_message(LOG_LEVEL_WARNING,
"write-devices: %s skipped: super-user activities are not permitted on this "
"receiver",
file->path ? file->path : "(null)");
escaped_path ? escaped_path : "(null)");
free(escaped_path);
return FILE_SAVE_SKIPPED;
}
return file_save_write_device(root_directory, file);
+67 -58
View File
@@ -64,10 +64,10 @@ void identity_clear_active(void) {
identity_active_reset();
}
void identity_set_active(const Config* config) {
bool identity_set_active(const Config* config) {
identity_active_reset();
if (!config)
return;
return true;
g_identity.numeric_ids = config->numeric_ids;
g_identity.chown_uid_set = config->chown_uid_set;
g_identity.chown_uid = config->chown_uid;
@@ -79,19 +79,19 @@ void identity_set_active(const Config* config) {
g_identity.copy_as_gid = config->copy_as_gid;
if (config->usermap_count > 0) {
g_identity.usermap = calloc((size_t)config->usermap_count, sizeof(IdentityMap));
if (g_identity.usermap) {
memcpy(g_identity.usermap, config->usermap,
(size_t)config->usermap_count * sizeof(IdentityMap));
g_identity.usermap_count = config->usermap_count;
}
if (!g_identity.usermap)
goto alloc_failed;
memcpy(g_identity.usermap, config->usermap,
(size_t)config->usermap_count * sizeof(IdentityMap));
g_identity.usermap_count = config->usermap_count;
}
if (config->groupmap_count > 0) {
g_identity.groupmap = calloc((size_t)config->groupmap_count, sizeof(IdentityMap));
if (g_identity.groupmap) {
memcpy(g_identity.groupmap, config->groupmap,
(size_t)config->groupmap_count * sizeof(IdentityMap));
g_identity.groupmap_count = config->groupmap_count;
}
if (!g_identity.groupmap)
goto alloc_failed;
memcpy(g_identity.groupmap, config->groupmap,
(size_t)config->groupmap_count * sizeof(IdentityMap));
g_identity.groupmap_count = config->groupmap_count;
}
g_identity.set = true;
/* A root receiver would honor any client-supplied ownership request (a
@@ -113,6 +113,15 @@ void identity_set_active(const Config* config) {
"--super requested but the receiver is not privileged; super-user "
"activities (ownership, device nodes) will be attempted but refused "
"by the kernel and skipped per entry");
return true;
alloc_failed:
/* Never proceed with a partial (count-left-zero) map: that would silently
apply the WRONG ownership policy. Fail closed and let the caller refuse
the connection. */
log_message(LOG_LEVEL_ERROR, "memory allocation failed while activating identity policy");
identity_active_reset();
return false;
}
bool privilege_super_permitted(void) {
@@ -440,6 +449,25 @@ static bool identity_id_fits_int32(unsigned long id) {
return id <= (unsigned long)INT32_MAX;
}
/* Resolve one --copy-as id token. A '*' token means the caller's current
* effective uid (user) or gid (group). Returns 0 on success. On failure sets
* *overflow when a '*' id was wider than int32 so the caller can log the
* specific message; otherwise the token was simply unresolvable. */
static int identity_resolve_copy_as_id(const char* token, bool is_group, int32_t* out,
bool* overflow) {
*overflow = false;
if (strcmp(token, "*") == 0) {
unsigned long current = is_group ? (unsigned long)getegid() : (unsigned long)geteuid();
if (!identity_id_fits_int32(current)) {
*overflow = true;
return -1;
}
*out = (int32_t)current;
return 0;
}
return identity_resolve_token(token, is_group, out);
}
int identity_parse_copy_as(Config* config, const char* value) {
if (!config || !value || *value == '\0') {
log_message(LOG_LEVEL_ERROR, "--copy-as requires USER[:GROUP]");
@@ -465,7 +493,7 @@ int identity_parse_copy_as(Config* config, const char* value) {
log_message(LOG_LEVEL_ERROR, "memory allocation failed for --copy-as");
return -1;
}
char* user_token = spec;
const char* user_token = spec;
const char* group_token = NULL;
char* colon = strchr(spec, ':');
if (colon) {
@@ -477,57 +505,39 @@ int identity_parse_copy_as(Config* config, const char* value) {
* (8-bit-safe) so a control byte cannot forge a log line. */
char* escaped_spec = output_escape(value, false);
const char* shown = escaped_spec ? escaped_spec : "<allocation failed>";
int ret = -1;
int32_t uid;
if (*user_token == '\0') {
log_message(LOG_LEVEL_ERROR, "--copy-as is missing the user (got '%s')", shown);
free(escaped_spec);
free(spec);
return -1;
goto done;
}
if (strcmp(user_token, "*") == 0) {
/* '*' means the current/root user: the client's euid. */
if (!identity_id_fits_int32((unsigned long)geteuid())) {
bool overflow = false;
int32_t uid;
if (identity_resolve_copy_as_id(user_token, false, &uid, &overflow) != 0) {
if (overflow)
log_message(LOG_LEVEL_ERROR, "--copy-as: current user id %lu exceeds INT32_MAX",
(unsigned long)geteuid());
free(escaped_spec);
free(spec);
return -1;
}
uid = (int32_t)geteuid();
} else if (identity_resolve_token(user_token, false, &uid) != 0) {
log_message(LOG_LEVEL_ERROR,
"--copy-as could not resolve user (use a name that exists on the "
"source, '*', or @N): %s",
shown);
free(escaped_spec);
free(spec);
return -1;
else
log_message(LOG_LEVEL_ERROR,
"--copy-as could not resolve user (use a name that exists on the "
"source, '*', or @N): %s",
shown);
goto done;
}
int32_t gid;
if (group_token) {
if (*group_token == '\0') {
log_message(LOG_LEVEL_ERROR, "--copy-as group is empty (got '%s')", shown);
free(escaped_spec);
free(spec);
return -1;
goto done;
}
if (strcmp(group_token, "*") == 0) {
if (!identity_id_fits_int32((unsigned long)getegid())) {
if (identity_resolve_copy_as_id(group_token, true, &gid, &overflow) != 0) {
if (overflow)
log_message(LOG_LEVEL_ERROR, "--copy-as: current group id %lu exceeds INT32_MAX",
(unsigned long)getegid());
free(escaped_spec);
free(spec);
return -1;
}
gid = (int32_t)getegid();
} else if (identity_resolve_token(group_token, true, &gid) != 0) {
log_message(LOG_LEVEL_ERROR, "--copy-as could not resolve group (got '%s'): %s", shown,
shown);
free(escaped_spec);
free(spec);
return -1;
else
log_message(LOG_LEVEL_ERROR, "--copy-as could not resolve group (got '%s')", shown);
goto done;
}
} else {
/* Group omitted: use the user's primary gid. A numeric id with no local
@@ -539,9 +549,7 @@ int identity_parse_copy_as(Config* config, const char* value) {
log_message(LOG_LEVEL_ERROR,
"--copy-as: primary group id %lu for the requested user exceeds INT32_MAX",
(unsigned long)pw->pw_gid);
free(escaped_spec);
free(spec);
return -1;
goto done;
}
gid = (int32_t)pw->pw_gid;
} else {
@@ -553,12 +561,8 @@ int identity_parse_copy_as(Config* config, const char* value) {
* identity_resolve_token. */
if (uid < 0 || gid < 0) {
log_message(LOG_LEVEL_ERROR, "--copy-as resolved id does not fit in int32 (got '%s')", shown);
free(escaped_spec);
free(spec);
return -1;
goto done;
}
free(escaped_spec);
free(spec);
config->copy_as_set = true;
config->copy_as_uid = uid;
@@ -566,7 +570,12 @@ int identity_parse_copy_as(Config* config, const char* value) {
/* Ownership application needs the metadata path (the source uid/gid must be
* transmitted); imply it exactly like --chown/--usermap/--groupmap. */
config->use_metadata = true;
return 0;
ret = 0;
done:
free(escaped_spec);
free(spec);
return ret;
}
/* ---- Receiver-side ownership application ---- */
+7 -2
View File
@@ -67,8 +67,13 @@ bool identity_copy_as_active(void);
/* Receiver-side snapshot of the negotiated identity config. The server calls
* identity_set_active() once per connection (before any file write) using the
* config received over the wire; the snapshot is a deep copy so the caller may
* free its Config immediately. identity_clear_active() releases it. */
void identity_set_active(const Config* config);
* free its Config immediately. identity_clear_active() releases it.
*
* Returns true on success. On an allocation failure while deep-copying a
* requested usermap/groupmap it logs a LOG_LEVEL_ERROR, leaves the snapshot
* cleared (never a partial/wrong policy) and returns false; the caller must
* refuse the connection. */
bool identity_set_active(const Config* config);
void identity_clear_active(void);
/* True when any ownership-affecting identity option is present in the active
+7 -3
View File
@@ -459,10 +459,14 @@ static char* protocol_receive_str_impl(ProtocolSession* session, bool redact) {
return NULL;
}
data[size] = '\0';
if (redact)
if (redact) {
log_debug_message(LOG_DEBUG_PROTO, "Received String: <redacted>");
else
log_debug_message(LOG_DEBUG_PROTO, "Received String: %s", data);
} else {
char* escaped_data = output_escape(data, log_get_8_bit_output());
log_debug_message(LOG_DEBUG_PROTO, "Received String: %s",
escaped_data ? escaped_data : "<allocation failed>");
free(escaped_data);
}
return data;
}