diff --git a/src/shared/compression.c b/src/shared/compression.c index d9aa037..7609921 100644 --- a/src/shared/compression.c +++ b/src/shared/compression.c @@ -17,10 +17,6 @@ static char* SKIP_COMPRESSION_EXTENSIONS[] = {".jpg", ".jpeg", ".png", ".gif", ".mp4", ".mkv", ".zip", ".gz", ".xz", ".zst", NULL}; -bool compression_should_skip(const char* path) { - return compression_should_skip_with_suffixes(path, NULL, -1); -} - bool compression_should_skip_with_suffixes(const char* path, char* const* suffixes, int count) { if (!path) return false; diff --git a/src/shared/compression.h b/src/shared/compression.h index 179c2b7..2c3753c 100644 --- a/src/shared/compression.h +++ b/src/shared/compression.h @@ -11,7 +11,6 @@ Data* data_compress_with_threads(Data* data_to_compress, int compression_level, int compression_threads); Data* data_decompress(Data* compressed_data); Data* data_decompress_limited(Data* compressed_data, size_t maximum_size); -bool compression_should_skip(const char* path); bool compression_should_skip_with_suffixes(const char* path, char* const* suffixes, int count); /* Release the calling thread's cached zstd contexts (compressor, decompressor diff --git a/src/shared/file.c b/src/shared/file.c index 6220967..b85e988 100644 --- a/src/shared/file.c +++ b/src/shared/file.c @@ -287,11 +287,6 @@ size_t file_content_to_buffer(File* file) { static int authorized_root_fd = -1; static char* authorized_root_path; -static bool path_is_within_root(const char* root, const char* path) { - size_t root_len = strlen(root); - return strncmp(root, path, root_len) == 0 && (path[root_len] == '\0' || path[root_len] == '/'); -} - 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) { @@ -362,10 +357,6 @@ void file_set_keep_dirlinks(bool enable) { file_keep_dirlinks = enable; } -bool file_get_keep_dirlinks(void) { - return file_keep_dirlinks; -} - /* --trust-sender (Phase 5) receiver process-wide policy: when set, the receiver * trusts the sender's file list and skips its own redundant up-front re- * validation (empty/".." path rejection, escaping-symlink-target containment). diff --git a/src/shared/file.h b/src/shared/file.h index 554170b..570d703 100644 --- a/src/shared/file.h +++ b/src/shared/file.h @@ -52,7 +52,6 @@ bool file_symlink_at_secure(const char* path, const char* target); /* --keep-dirlinks (-K) receiver process-wide policy: allow an in-root existing * symlink-to-directory to be followed as a directory. */ void file_set_keep_dirlinks(bool enable); -bool file_get_keep_dirlinks(void); /* --trust-sender receiver process-wide policy (Phase 5). When set, the * receiver trusts that the sender already produced a clean file list and skips diff --git a/src/shared/file_store.c b/src/shared/file_store.c index b14da25..5365598 100644 --- a/src/shared/file_store.c +++ b/src/shared/file_store.c @@ -1,129 +1,7 @@ #include -#include -#include -#include -#include -#include -#include #include #include "file_store.h" -#include "metadata.h" -#include "utils.h" - -static int authorized_root_fd = -1; -static char* authorized_root_path; - -static bool path_is_within_root(const char* root, const char* path) { - size_t root_length = strlen(root); - return strncmp(root, path, root_length) == 0 && - (path[root_length] == '\0' || path[root_length] == '/'); -} - -bool file_store_set_authorized_root(int fd, const char* canonical_path) { - char* new_path = canonical_path ? str_dup(canonical_path) : NULL; - if (canonical_path && !new_path) { - authorized_root_fd = -1; - free(authorized_root_path); - authorized_root_path = NULL; - return false; - } - free(authorized_root_path); - authorized_root_path = new_path; - authorized_root_fd = fd; - return true; -} - -int file_store_open_secure_parent(const char* path, char** leaf_out) { - char* copy = str_dup(path); - if (!copy) - return -1; - char* parent = dirname(copy); - const char* slash = strrchr(path, '/'); - char* leaf = str_dup(slash ? slash + 1 : path); - if (!leaf) { - free(copy); - return -1; - } - int fd; - if (authorized_root_fd >= 0) { - if (!authorized_root_path || path[0] != '/' || - !path_is_within_root(authorized_root_path, path)) { - free(copy); - free(leaf); - return -1; - } - fd = dup(authorized_root_fd); - if (fd < 0) { - free(copy); - free(leaf); - return -1; - } - size_t root_length = strlen(authorized_root_path); - char* relative = str_dup(path + root_length); - if (!relative) { - free(copy); - free(leaf); - close(fd); - return -1; - } - free(copy); - copy = relative; - parent = dirname(copy); - } else { - fd = (parent[0] == '/') ? open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) - : open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); - } - if (fd < 0) { - free(copy); - free(leaf); - return -1; - } - char* save = NULL; - char* component = strtok_r(parent, "/", &save); - while (component) { - if (strcmp(component, "..") == 0) { - close(fd); - free(copy); - free(leaf); - return -1; - } - if (strcmp(component, ".") != 0) { - int next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); - if (next < 0 && errno == ENOENT) { - if (mkdirat(fd, component, 0755) == 0 || errno == EEXIST) - next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); - } - if (next < 0) { - close(fd); - free(copy); - free(leaf); - return -1; - } - close(fd); - fd = next; - } - component = strtok_r(NULL, "/", &save); - } - free(copy); - *leaf_out = leaf; - return fd; -} - -bool file_store_rename_secure(const char* old_path, const char* new_path) { - char *old_leaf = NULL, *new_leaf = NULL; - int old_parent = file_store_open_secure_parent(old_path, &old_leaf); - int new_parent = file_store_open_secure_parent(new_path, &new_leaf); - bool ok = old_parent >= 0 && new_parent >= 0 && - renameat(old_parent, old_leaf, new_parent, new_leaf) == 0; - if (old_parent >= 0) - close(old_parent); - if (new_parent >= 0) - close(new_parent); - free(old_leaf); - free(new_leaf); - return ok; -} static bool write_all(int fd, const void* data, unsigned long long size) { const unsigned char* p = data; @@ -176,67 +54,3 @@ bool file_store_write_sparse(int fd, const unsigned char* data, unsigned long lo } return ftruncate(fd, (off_t)size) == 0; } - -bool file_store_write_secure(const char* path, const void* data, unsigned long long data_size, - bool inplace, bool sparse, const FileMetadata* metadata, - bool preserve_executability) { - char* leaf = NULL; - int dirfd = file_store_open_secure_parent(path, &leaf); - if (dirfd < 0) - return false; - int fd = -1; - bool ok = false; - if (inplace) { - fd = openat(dirfd, leaf, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0644); - if (fd >= 0) { - if (sparse && data_size > 0) { - if (ftruncate(fd, (off_t)data_size) == 0) - ok = file_store_write_sparse(fd, data, data_size); - } else { - ok = write_all(fd, data, data_size); - } - if (ok && metadata) - ok = file_restore_metadata_fd(fd, metadata, preserve_executability); - } - } else { - int tmp_size = snprintf(NULL, 0, ".%s.tmp.%ld.%u", leaf, (long)getpid(), 99U); - if (tmp_size < 0) { - close(dirfd); - free(leaf); - return false; - } - char* tmp = malloc((size_t)tmp_size + 1); - if (!tmp) { - close(dirfd); - free(leaf); - return false; - } - for (unsigned int i = 0; i < 100 && !ok; ++i) { - snprintf(tmp, (size_t)tmp_size + 1, ".%s.tmp.%ld.%u", leaf, (long)getpid(), i); - fd = openat(dirfd, tmp, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600); - if (fd < 0) - continue; - if (sparse && data_size > 0) - ok = ftruncate(fd, (off_t)data_size) == 0; - if (ok || (!sparse || data_size == 0)) - ok = (sparse && data_size > 0) - ? 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 (close(fd) != 0) - ok = false; - fd = -1; - if (ok && renameat(dirfd, tmp, dirfd, leaf) != 0) - ok = false; - if (!ok) - unlinkat(dirfd, tmp, 0); - } - free(tmp); - } - if (fd >= 0) - close(fd); - close(dirfd); - free(leaf); - return ok; -} diff --git a/src/shared/file_store.h b/src/shared/file_store.h index 4bfb39a..9514648 100644 --- a/src/shared/file_store.h +++ b/src/shared/file_store.h @@ -1,15 +1,8 @@ #ifndef FILE_STORE_H #define FILE_STORE_H -#include "file.h" #include -bool file_store_set_authorized_root(int fd, const char* canonical_path); -int file_store_open_secure_parent(const char* path, char** leaf_out); -bool file_store_rename_secure(const char* old_path, const char* new_path); -bool file_store_write_secure(const char* path, const void* data, unsigned long long data_size, - bool inplace, bool sparse, const FileMetadata* metadata, - bool preserve_executability); /* Sparse-aware write (--sparse/-S): every all-zero run of at least * SPARSE_HOLE_MIN bytes is skipped with lseek(SEEK_CUR) so it becomes a real * hole; every other byte is written. The caller pre-sizes the file with diff --git a/src/shared/protocol.c b/src/shared/protocol.c index 3c3eee0..f3dd81b 100644 --- a/src/shared/protocol.c +++ b/src/shared/protocol.c @@ -570,10 +570,6 @@ Data* protocol_receive_data_limited(ProtocolSession* session, unsigned long long return result; } -Data* protocol_receive_data(ProtocolSession* session) { - return protocol_receive_data_limited(session, MAX_DATA_PAYLOAD_SIZE); -} - bool protocol_send_int(ProtocolSession* session, int data) { if (!protocol_send_n_data(session, &data, sizeof(int))) return false; diff --git a/src/shared/protocol.h b/src/shared/protocol.h index 60f6dec..b27c2d1 100644 --- a/src/shared/protocol.h +++ b/src/shared/protocol.h @@ -172,7 +172,6 @@ char* protocol_receive_str(ProtocolSession* session); bool protocol_send_str_redacted(ProtocolSession* session, const char* data); char* protocol_receive_str_redacted(ProtocolSession* session); bool protocol_send_data(ProtocolSession* session, const Data* data); -Data* protocol_receive_data(ProtocolSession* session); Data* protocol_receive_data_limited(ProtocolSession* session, unsigned long long maximum_size); bool protocol_send_int(ProtocolSession* session, int data); bool protocol_receive_int(ProtocolSession* session, int* data); diff --git a/src/shared/transport_tcp.c b/src/shared/transport_tcp.c index 42dad4c..2758cbe 100644 --- a/src/shared/transport_tcp.c +++ b/src/shared/transport_tcp.c @@ -440,10 +440,6 @@ bool tcp_connect_socket_ex(Client* client, const char* host, int port, return true; } -bool tcp_connect_socket(Client* client, const char* host, int port) { - return tcp_connect_socket_ex(client, host, port, NULL); -} - bool client_connect_ex(Client* client, const char* host, int port, const TcpConnectOptions* opts) { if (!tcp_connect_socket_ex(client, host, port, opts)) return false; diff --git a/src/shared/transport_tcp.h b/src/shared/transport_tcp.h index 4439003..e37b879 100644 --- a/src/shared/transport_tcp.h +++ b/src/shared/transport_tcp.h @@ -57,7 +57,6 @@ bool client_connect_ex(Client* client, const char* host, int port, const TcpConn bool client_connect(Client* client, const char* host, int port); bool tcp_connect_socket_ex(Client* client, const char* host, int port, const TcpConnectOptions* opts); -bool tcp_connect_socket(Client* client, const char* host, int port); void client_disconnect(Client* client); void client_delete(Client* client); void tcp_set_timeouts(int timeout_sec, int contimeout_sec); diff --git a/src/shared/utils.c b/src/shared/utils.c index 080785d..d790172 100644 --- a/src/shared/utils.c +++ b/src/shared/utils.c @@ -36,11 +36,19 @@ void utils_set_authorized_root_fd(int fd) { (void)utils_set_authorized_root(fd, NULL); } -static 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); return strncmp(root, path, root_len) == 0 && (path[root_len] == '\0' || path[root_len] == '/'); } +/* Open the destination root directory itself, confined to the authorized root. + * NOTE (do not merge with file_open_secure_parent): this walk opens dest_root + * (a directory that must already exist) and returns its fd, whereas + * file_open_secure_parent resolves the PARENT of a file path, optionally + * creating missing components and honouring --keep-dirlinks / --copy-as. The + * two differ in create-vs-no-create, in what path component they stop at, and + * in the extra receiver policies they apply, so they are intentionally kept + * separate. Both rely on the shared lexical path_is_within_root check. */ static int open_authorized_destination(const char* dest_root) { if (authorized_root_fd < 0 || !authorized_root_path || !dest_root || !path_is_within_root(authorized_root_path, dest_root)) diff --git a/src/shared/utils.h b/src/shared/utils.h index e97c635..f4a5bd6 100644 --- a/src/shared/utils.h +++ b/src/shared/utils.h @@ -127,6 +127,12 @@ bool utils_set_authorized_root(int fd, const char* canonical_path); /* The fd-only compatibility form is fail-closed for path-based operations; * callers should use utils_set_authorized_root with the canonical identity. */ void utils_set_authorized_root_fd(int fd); +/* 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` + * and `path` must be absolute canonical paths free of "."/".." components (the + * callers guarantee this); this is containment by string, not by resolved + * symlinks. Shared by the utils and file secure-walk root confinement. */ +bool path_is_within_root(const char* root, const char* path); bool has_path_traversal(const char* path); bool utils_valid_batch_path(const char* path); bool format_human_bytes(unsigned long long bytes, char* buffer, size_t buffer_size); diff --git a/tests/test_file.c b/tests/test_file.c index 4d53b70..4561f1a 100644 --- a/tests/test_file.c +++ b/tests/test_file.c @@ -3,7 +3,6 @@ #endif #include "test_file.h" #include "file.h" -#include "file_store.h" #include "file_receive.h" #include "data.h" #include "config.h" @@ -1225,7 +1224,7 @@ void test_trust_sender() { } /* --sparse/-S hole preservation: a buffer with a long zero run written via - * file_store_write_secure(sparse=true) must round-trip its content exactly and + * file_to_disk_secure(sparse=true) must round-trip its content exactly and * have the right logical size, and should additionally be genuinely sparse on * filesystems that support holes. The sparseness assertion is tolerant: if the * filesystem reports no holes (SEEK_HOLE/SEEK_DATA -> ENXIO) we skip the strict @@ -1247,7 +1246,7 @@ static void test_file_write_to_disk_sparse_preserves_holes() { buf[size - 1 - i] = (unsigned char)((i * 7) % 253); } - EXPECT_TRUE(file_store_write_secure(path, buf, size, false, true, NULL, false)); + EXPECT_TRUE(file_to_disk_secure(path, buf, size, false, true, false, NULL, false, NULL)); /* Logical size must equal data_size exactly. */ struct stat st;