fix(receiver): non-blocking receiver opens, inplace type gate, dry-run/B4/B5/B6
Address confirmed receiver security findings B1-B6: B1 (HIGH): add O_NONBLOCK to the three receiver read-opens that opened an existing destination/basis entry before the S_ISREG gate (incremental_check_open_destination, basis_open_regular, hardlink_read_source) so a client-planted FIFO can no longer block the receive thread forever while the post-open type gate still rejects it. B2 (HIGH/MED): --inplace now fstatat(AT_SYMLINK_NOFOLLOW)-probes the target and refuses any existing non-regular entry, opens with O_NONBLOCK, and re-checks S_ISREG on the opened fd. This stops a FIFO from hanging the open and stops a char/block device from being written directly (bypassing --write-devices). B3 (MED): under --dry-run the incremental quick-skip no longer reads/hashes the destination file for --checksum/--delta; it decides from metadata only and reports would-transfer when the comparison is inconclusive, closing the read-only-module content-hash oracle. B4 (LOW): xattr_name_appliable() now gates the two system.posix_acl_* names on preserve_acls (--acls), not the derived use_xattrs (--xattrs OR --acls). The receiver drops (never applies) ACL entries when -A was not negotiated while keeping user.* working for -X. B5 (INFO): receive_manifest_section() charges a per-entry overhead against MAX_MANIFEST_BYTES and the aggregate entry count across all three sections is capped at MAX_MANIFEST_ENTRIES. B6 (MED): data_charge_session() reserves decompressed/chunk-copy bytes against the owning ProtocolSession (MAX_CONNECTION_MEMORY) and records them on the Data so data_destroy() releases them via the Data.owner path. Applied to the whole-file/append/delta decompression sites and chunk_deserialize() per-file copies; a missing session owner degrades to the previous uncharged behavior. Tests: FIFO destination/basis non-hang (with alarm), --inplace FIFO/device refusal, dry-run no-read oracle test plus updated metadata-only dry-run tests, ACL-without--acls drop, manifest total-entry cap, and chunk session charging.
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
@@ -994,6 +995,94 @@ static void test_inplace_overwrite_truncates_shorter_payload() {
|
||||
rmdir(root);
|
||||
}
|
||||
|
||||
/* B2: --inplace must refuse an existing non-regular destination entry. A FIFO
|
||||
would block open(O_WRONLY) forever and a device node would be written
|
||||
directly, bypassing the --write-devices/super gate. Forked with an alarm so
|
||||
a regression is a prompt failure instead of a hung suite. */
|
||||
static void test_inplace_refuses_fifo_destination() {
|
||||
const char* root = "test_inplace_fifo_tmp";
|
||||
const char* path = "test_inplace_fifo_tmp/fifo";
|
||||
unlink(path);
|
||||
rmdir(root);
|
||||
EXPECT_EQ_INT(mkdir(root, 0700), 0);
|
||||
EXPECT_EQ_INT(mkfifo(path, 0600), 0);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
alarm(10);
|
||||
File* f = file_create("fifo");
|
||||
if (!f)
|
||||
_exit(1);
|
||||
const char* content = "payload";
|
||||
f->data->data = malloc(strlen(content));
|
||||
if (!f->data->data)
|
||||
_exit(1);
|
||||
memcpy(f->data->data, content, strlen(content));
|
||||
f->data->size = strlen(content);
|
||||
Config* cfg = config_create();
|
||||
if (!cfg)
|
||||
_exit(1);
|
||||
cfg->inplace = true;
|
||||
bool written = file_save_to_disk(root, f, cfg);
|
||||
file_destroy(f);
|
||||
config_delete(cfg);
|
||||
_exit(written ? 1 : 0); /* must be refused */
|
||||
}
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(lstat(path, &st), 0);
|
||||
EXPECT_TRUE(S_ISFIFO(st.st_mode)); /* left untouched */
|
||||
unlink(path);
|
||||
rmdir(root);
|
||||
}
|
||||
|
||||
/* B2: an existing char device must not be written by --inplace. mknod needs
|
||||
privilege, so a non-root run skips gracefully. /dev/null's (1:3) rdev makes
|
||||
the negative case harmless if it ever regresses. */
|
||||
static void test_inplace_refuses_device_destination() {
|
||||
const char* root = "test_inplace_dev_tmp";
|
||||
const char* path = "test_inplace_dev_tmp/dev";
|
||||
unlink(path);
|
||||
rmdir(root);
|
||||
EXPECT_EQ_INT(mkdir(root, 0700), 0);
|
||||
if (mknod(path, S_IFCHR | 0600, makedev(1, 3)) != 0) {
|
||||
rmdir(root);
|
||||
return; /* no privilege to create a device node: skip */
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
alarm(10);
|
||||
File* f = file_create("dev");
|
||||
if (!f)
|
||||
_exit(1);
|
||||
const char* content = "payload";
|
||||
f->data->data = malloc(strlen(content));
|
||||
if (!f->data->data)
|
||||
_exit(1);
|
||||
memcpy(f->data->data, content, strlen(content));
|
||||
f->data->size = strlen(content);
|
||||
Config* cfg = config_create();
|
||||
if (!cfg)
|
||||
_exit(1);
|
||||
cfg->inplace = true;
|
||||
bool written = file_save_to_disk(root, f, cfg);
|
||||
file_destroy(f);
|
||||
config_delete(cfg);
|
||||
_exit(written ? 1 : 0); /* must be refused */
|
||||
}
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(lstat(path, &st), 0);
|
||||
EXPECT_TRUE(S_ISCHR(st.st_mode)); /* still a device, not replaced */
|
||||
unlink(path);
|
||||
rmdir(root);
|
||||
}
|
||||
|
||||
/* Explicit directory entries (--dirs) create the directory under the receive
|
||||
root through the same save funnel, creating parents as needed, and reject
|
||||
traversal the same way a file path does. */
|
||||
@@ -1606,4 +1695,6 @@ void test_file() {
|
||||
test_inplace_overwrite_clears_special_mode_bits();
|
||||
test_inplace_overwrite_metadata_strips_special_bits();
|
||||
test_inplace_overwrite_truncates_shorter_payload();
|
||||
test_inplace_refuses_fifo_destination();
|
||||
test_inplace_refuses_device_destination();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user