Merge branch 'fix/w8-authroot' into fix/w8-integration

This commit is contained in:
2026-09-13 11:13:40 +02:00
6 changed files with 67 additions and 74 deletions
+7 -27
View File
@@ -30,8 +30,6 @@
#include <time.h> #include <time.h>
#include <openssl/x509.h> #include <openssl/x509.h>
static char* authorized_root;
static int authorized_root_fd = -1;
static bool allow_delete; static bool allow_delete;
static bool trust_sender; static bool trust_sender;
static bool allow_unauthenticated; static bool allow_unauthenticated;
@@ -201,13 +199,10 @@ static bool tls_client_identity_allowed(SSL* ssl) {
} }
static void release_authorization(void) { static void release_authorization(void) {
file_set_authorized_root(-1, NULL); int root_fd = utils_get_authorized_root_fd();
utils_set_authorized_root_fd(-1); utils_set_authorized_root(-1, NULL);
if (authorized_root_fd >= 0) if (root_fd >= 0)
close(authorized_root_fd); close(root_fd);
authorized_root_fd = -1;
free(authorized_root);
authorized_root = NULL;
} }
static bool path_is_within(const char* root, const char* path) { static bool path_is_within(const char* root, const char* path) {
@@ -233,13 +228,11 @@ static bool ensure_receive_root(const Config* config) {
static bool configure_authorization(const char* root) { static bool configure_authorization(const char* root) {
char resolved[PATH_MAX]; char resolved[PATH_MAX];
if (!root) { if (!root) {
file_set_authorized_root(-1, NULL);
utils_set_authorized_root(-1, NULL); utils_set_authorized_root(-1, NULL);
return false; return false;
} }
int root_fd = open(root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); int root_fd = open(root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (root_fd < 0) { if (root_fd < 0) {
file_set_authorized_root(-1, NULL);
utils_set_authorized_root(-1, NULL); utils_set_authorized_root(-1, NULL);
return false; return false;
} }
@@ -248,26 +241,12 @@ static bool configure_authorization(const char* root) {
if (fd_path_length < 0 || (size_t)fd_path_length >= sizeof(fd_path) || if (fd_path_length < 0 || (size_t)fd_path_length >= sizeof(fd_path) ||
!realpath(fd_path, resolved)) { !realpath(fd_path, resolved)) {
close(root_fd); close(root_fd);
file_set_authorized_root(-1, NULL);
utils_set_authorized_root(-1, NULL); utils_set_authorized_root(-1, NULL);
return false; return false;
} }
authorized_root = str_dup(resolved); if (!utils_set_authorized_root(root_fd, resolved)) {
if (!authorized_root) { /* The setter already cleared the fd/path state on allocation failure. */
close(root_fd); close(root_fd);
file_set_authorized_root(-1, NULL);
utils_set_authorized_root(-1, NULL);
return false;
}
authorized_root_fd = root_fd;
if (!file_set_authorized_root(authorized_root_fd, authorized_root) ||
!utils_set_authorized_root(authorized_root_fd, authorized_root)) {
file_set_authorized_root(-1, NULL);
utils_set_authorized_root(-1, NULL);
close(authorized_root_fd);
authorized_root_fd = -1;
free(authorized_root);
authorized_root = NULL;
return false; return false;
} }
return true; return true;
@@ -713,6 +692,7 @@ void handler(int file_descriptor) {
* in effect. A client's --timeout tightens only that client's own protocol * in effect. A client's --timeout tightens only that client's own protocol
* I/O and the server's socket read/write timeout is the transport default. */ * I/O and the server's socket read/write timeout is the transport default. */
protocol_session_set_io_timeout(&session, config->timeout); protocol_session_set_io_timeout(&session, config->timeout);
const char* authorized_root = utils_get_authorized_root_path();
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");
goto done; goto done;
+17 -29
View File
@@ -284,23 +284,6 @@ size_t file_content_to_buffer(File* file) {
/* ---- Secure filesystem primitives ---- */ /* ---- Secure filesystem primitives ---- */
static int authorized_root_fd = -1;
static char* authorized_root_path;
bool file_set_authorized_root(int fd, const char* canonical_path) {
char* path_copy = canonical_path ? str_dup(canonical_path) : NULL;
if (canonical_path && !path_copy) {
authorized_root_fd = -1;
free(authorized_root_path);
authorized_root_path = NULL;
return false;
}
authorized_root_fd = fd;
free(authorized_root_path);
authorized_root_path = path_copy;
return true;
}
bool file_path_exists_secure(const char* path) { bool file_path_exists_secure(const char* path) {
if (!path) if (!path)
return false; return false;
@@ -483,7 +466,10 @@ static int open_dir_beneath_root(const char* resolved, const char* root) {
rel++; rel++;
if (*rel == '\0') if (*rel == '\0')
return -1; return -1;
int fd = dup(authorized_root_fd); int root_fd = utils_get_authorized_root_fd();
if (root_fd < 0)
return -1;
int fd = dup(root_fd);
if (fd < 0) if (fd < 0)
return -1; return -1;
char* copy = str_dup(rel); char* copy = str_dup(rel);
@@ -525,20 +511,21 @@ int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs)
return -1; return -1;
} }
int fd; int fd;
if (authorized_root_fd >= 0) { int root_fd = utils_get_authorized_root_fd();
if (!authorized_root_path || path[0] != '/' || const char* root_path = utils_get_authorized_root_path();
!path_is_within_root(authorized_root_path, path)) { if (root_fd >= 0) {
if (!root_path || path[0] != '/' || !path_is_within_root(root_path, path)) {
free(copy); free(copy);
free(leaf); free(leaf);
return -1; return -1;
} }
fd = dup(authorized_root_fd); fd = dup(root_fd);
if (fd < 0) { if (fd < 0) {
free(copy); free(copy);
free(leaf); free(leaf);
return -1; return -1;
} }
size_t root_len = strlen(authorized_root_path); size_t root_len = strlen(root_path);
char* relative = str_dup(path + root_len); char* relative = str_dup(path + root_len);
if (!relative) { if (!relative) {
free(copy); free(copy);
@@ -602,15 +589,14 @@ int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs)
O_NOFOLLOW walk. Only honoured when the symlink resolves to a O_NOFOLLOW walk. Only honoured when the symlink resolves to a
directory that stays beneath the authorized root, so a malicious link directory that stays beneath the authorized root, so a malicious link
can never redirect the write outside it. */ can never redirect the write outside it. */
if (next < 0 && file_keep_dirlinks && authorized_root_path != NULL && if (next < 0 && file_keep_dirlinks && root_path != NULL &&
(errno == ELOOP || errno == ENOTDIR || errno == EACCES)) { (errno == ELOOP || errno == ENOTDIR || errno == EACCES)) {
struct stat lst; struct stat lst;
if (fstatat(fd, component, &lst, AT_SYMLINK_NOFOLLOW) == 0 && S_ISLNK(lst.st_mode)) { if (fstatat(fd, component, &lst, AT_SYMLINK_NOFOLLOW) == 0 && S_ISLNK(lst.st_mode)) {
char candidate[PATH_MAX]; char candidate[PATH_MAX];
char root[PATH_MAX]; char root[PATH_MAX];
if (realpath(authorized_root_path, root) && if (realpath(root_path, root) && snprintf(candidate, sizeof(candidate), "%s%s/%s", root,
snprintf(candidate, sizeof(candidate), "%s%s/%s", root, rel_buf, component) < rel_buf, component) < (int)sizeof(candidate)) {
(int)sizeof(candidate)) {
char resolved[PATH_MAX]; char resolved[PATH_MAX];
if (realpath(candidate, resolved) && strcmp(resolved, root) != 0 && if (realpath(candidate, resolved) && strcmp(resolved, root) != 0 &&
strncmp(root, resolved, strlen(root)) == 0 && strncmp(root, resolved, strlen(root)) == 0 &&
@@ -685,8 +671,9 @@ bool file_ensure_directory_secure(const char* path) {
return false; return false;
/* The authorized root is already an open directory, and the filesystem root /* The authorized root is already an open directory, and the filesystem root
is always present: there is no final component left to create for them. */ is always present: there is no final component left to create for them. */
const char* root_path = utils_get_authorized_root_path();
bool root_is_open = bool root_is_open =
authorized_root_fd >= 0 && authorized_root_path && strcmp(norm, authorized_root_path) == 0; utils_get_authorized_root_fd() >= 0 && root_path && strcmp(norm, root_path) == 0;
if (root_is_open || strcmp(norm, "/") == 0) { if (root_is_open || strcmp(norm, "/") == 0) {
free(norm); free(norm);
return true; return true;
@@ -733,8 +720,9 @@ bool file_directory_exists_secure(const char* path) {
char* norm = normalize_directory_path(path); char* norm = normalize_directory_path(path);
if (!norm) if (!norm)
return false; return false;
const char* root_path = utils_get_authorized_root_path();
bool root_is_open = bool root_is_open =
authorized_root_fd >= 0 && authorized_root_path && strcmp(norm, authorized_root_path) == 0; utils_get_authorized_root_fd() >= 0 && root_path && strcmp(norm, root_path) == 0;
if (root_is_open || strcmp(norm, "/") == 0) { if (root_is_open || strcmp(norm, "/") == 0) {
free(norm); free(norm);
return true; return true;
-3
View File
@@ -62,9 +62,6 @@ void file_set_keep_dirlinks(bool enable);
void file_set_trust_sender(bool enable); void file_set_trust_sender(bool enable);
bool file_get_trust_sender(void); bool file_get_trust_sender(void);
/* A configured fd without a canonical identity deliberately rejects paths. */
bool file_set_authorized_root(int fd, const char* canonical_path);
/* Secure path/filesystem primitives (symlink-safe, O_NOFOLLOW, root-confined). */ /* Secure path/filesystem primitives (symlink-safe, O_NOFOLLOW, root-confined). */
bool file_path_exists_secure(const char* path); bool file_path_exists_secure(const char* path);
bool file_stat_secure(const char* path, struct stat* st); bool file_stat_secure(const char* path, struct stat* st);
+20 -7
View File
@@ -36,6 +36,17 @@ void utils_set_authorized_root_fd(int fd) {
(void)utils_set_authorized_root(fd, NULL); (void)utils_set_authorized_root(fd, NULL);
} }
/* Accessors for the process-global authorized root. The path pointer is
* borrowed and valid until the next setter call; the root is a single-threaded,
* set-before-worker-threads value (see server.c), so these carry no locking. */
int utils_get_authorized_root_fd(void) {
return authorized_root_fd;
}
const char* utils_get_authorized_root_path(void) {
return authorized_root_path;
}
bool path_is_within_root(const char* root, const char* path) { bool path_is_within_root(const char* root, const char* path) {
size_t root_len = strlen(root); size_t root_len = strlen(root);
return strncmp(root, path, root_len) == 0 && (path[root_len] == '\0' || path[root_len] == '/'); return strncmp(root, path, root_len) == 0 && (path[root_len] == '\0' || path[root_len] == '/');
@@ -50,15 +61,16 @@ bool path_is_within_root(const char* root, const char* path) {
* in the extra receiver policies they apply, so they are intentionally kept * in the extra receiver policies they apply, so they are intentionally kept
* separate. Both rely on the shared lexical path_is_within_root check. */ * separate. Both rely on the shared lexical path_is_within_root check. */
static int open_authorized_destination(const char* dest_root) { static int open_authorized_destination(const char* dest_root) {
if (authorized_root_fd < 0 || !authorized_root_path || !dest_root || int root_fd = utils_get_authorized_root_fd();
!path_is_within_root(authorized_root_path, dest_root)) const char* root_path = utils_get_authorized_root_path();
if (root_fd < 0 || !root_path || !dest_root || !path_is_within_root(root_path, dest_root))
return -1; return -1;
int dirfd = dup(authorized_root_fd); int dirfd = dup(root_fd);
if (dirfd < 0) if (dirfd < 0)
return -1; return -1;
const char* relative_path = dest_root + strlen(authorized_root_path); const char* relative_path = dest_root + strlen(root_path);
while (*relative_path == '/') while (*relative_path == '/')
relative_path++; relative_path++;
char* relative = str_dup(*relative_path ? relative_path : "."); char* relative = str_dup(*relative_path ? relative_path : ".");
@@ -689,11 +701,12 @@ DeleteWalkResult delete_extras_limited(const char* dest_root, const ArrayList* m
if (!build_keep_index(manifest, &keep)) if (!build_keep_index(manifest, &keep))
return DELETE_WALK_ERROR; return DELETE_WALK_ERROR;
int rootfd; int rootfd;
if (authorized_root_fd >= 0) { int root_fd = utils_get_authorized_root_fd();
if (authorized_root_path) if (root_fd >= 0) {
if (utils_get_authorized_root_path())
rootfd = open_authorized_destination(dest_root); rootfd = open_authorized_destination(dest_root);
else if (dest_root == NULL) else if (dest_root == NULL)
rootfd = dup(authorized_root_fd); rootfd = dup(root_fd);
else else
rootfd = -1; rootfd = -1;
} else { } else {
+15
View File
@@ -127,6 +127,21 @@ bool utils_set_authorized_root(int fd, const char* canonical_path);
/* The fd-only compatibility form is fail-closed for path-based operations; /* The fd-only compatibility form is fail-closed for path-based operations;
* callers should use utils_set_authorized_root with the canonical identity. */ * callers should use utils_set_authorized_root with the canonical identity. */
void utils_set_authorized_root_fd(int fd); void utils_set_authorized_root_fd(int fd);
/* Read accessors for the process-wide authorized root, so every secure-walk
* site consumes the single shared state instead of keeping its own copy. The
* fd is caller-owned (see the setters): it is returned verbatim, never dup'd,
* and the caller that opened it is responsible for closing it. With no root
* configured the fd accessor returns -1 and the path accessor returns NULL.
*
* The pointer returned by utils_get_authorized_root_path() is borrowed into
* process-global state and is invalidated by the next
* utils_set_authorized_root() / utils_set_authorized_root_fd() call. The fd
* and path are stored separately and read independently, so the pair is NOT
* observed atomically together; the accessors are non-reentrant and callers
* must serialize configuration (the server installs the root before any worker
* threads spawn; see utils.c). */
int utils_get_authorized_root_fd(void);
const char* utils_get_authorized_root_path(void);
/* True when `path` is `root` itself or lies directly beneath it: a lexical /* True when `path` is `root` itself or lies directly beneath it: a lexical
* prefix test requiring the byte after `root` to be '\0' or '/'. Both `root` * prefix test requiring the byte after `root` to be '\0' or '/'. Both `root`
* and `path` must be absolute canonical paths free of "."/".." components (the * and `path` must be absolute canonical paths free of "."/".." components (the
+8 -8
View File
@@ -1180,7 +1180,7 @@ static void test_trust_sender_authorized_root_confinement() {
rmdir(sibling); rmdir(sibling);
return; return;
} }
EXPECT_TRUE(file_set_authorized_root(root_fd, root_abs)); EXPECT_TRUE(utils_set_authorized_root(root_fd, root_abs));
file_set_trust_sender(true); file_set_trust_sender(true);
struct stat st; struct stat st;
@@ -1204,7 +1204,7 @@ static void test_trust_sender_authorized_root_confinement() {
free(outside_link); free(outside_link);
free(inside_link); free(inside_link);
file_set_authorized_root(-1, NULL); utils_set_authorized_root(-1, NULL);
close(root_fd); close(root_fd);
unlink("test_trust_sender_outside_link"); unlink("test_trust_sender_outside_link");
rmdir(sibling); rmdir(sibling);
@@ -1220,7 +1220,7 @@ void test_trust_sender() {
test_trust_sender_confines_hostile_paths(); test_trust_sender_confines_hostile_paths();
test_trust_sender_authorized_root_confinement(); test_trust_sender_authorized_root_confinement();
file_set_trust_sender(false); file_set_trust_sender(false);
file_set_authorized_root(-1, NULL); utils_set_authorized_root(-1, NULL);
} }
/* --sparse/-S hole preservation: a buffer with a long zero run written via /* --sparse/-S hole preservation: a buffer with a long zero run written via
@@ -1341,7 +1341,7 @@ static void test_file_write_to_disk_partial_retention() {
static void test_dir_time_list() { static void test_dir_time_list() {
const char* root = "test_dir_time_root"; const char* root = "test_dir_time_root";
const char* sub = "test_dir_time_root/sub"; const char* sub = "test_dir_time_root/sub";
file_set_authorized_root(-1, NULL); utils_set_authorized_root(-1, NULL);
rmdir(sub); rmdir(sub);
rmdir(root); rmdir(root);
EXPECT_EQ_INT(mkdir(root, 0755), 0); EXPECT_EQ_INT(mkdir(root, 0755), 0);
@@ -1485,7 +1485,7 @@ static void test_keep_dirlinks_secure_open_impl() {
rmdir(outside); rmdir(outside);
return; return;
} }
EXPECT_TRUE(file_set_authorized_root(root_fd, root_abs)); EXPECT_TRUE(utils_set_authorized_root(root_fd, root_abs));
file_set_keep_dirlinks(true); file_set_keep_dirlinks(true);
struct stat real_st; struct stat real_st;
@@ -1538,7 +1538,7 @@ static void test_keep_dirlinks_secure_open_impl() {
free(leaf); free(leaf);
file_set_keep_dirlinks(false); file_set_keep_dirlinks(false);
file_set_authorized_root(-1, NULL); utils_set_authorized_root(-1, NULL);
close(root_fd); close(root_fd);
unlink(link); unlink(link);
unlink(abslink); unlink(abslink);
@@ -1552,10 +1552,10 @@ static void test_keep_dirlinks_secure_open_impl() {
* cleared even when an EXPECT inside the body returns early (a failing EXPECT * cleared even when an EXPECT inside the body returns early (a failing EXPECT
* returns from its own function, so the body's trailing resets may be skipped). */ * returns from its own function, so the body's trailing resets may be skipped). */
static void test_keep_dirlinks_secure_open() { static void test_keep_dirlinks_secure_open() {
file_set_authorized_root(-1, NULL); utils_set_authorized_root(-1, NULL);
file_set_keep_dirlinks(false); file_set_keep_dirlinks(false);
test_keep_dirlinks_secure_open_impl(); test_keep_dirlinks_secure_open_impl();
file_set_authorized_root(-1, NULL); utils_set_authorized_root(-1, NULL);
file_set_keep_dirlinks(false); file_set_keep_dirlinks(false);
} }