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.
338 lines
11 KiB
C
338 lines
11 KiB
C
|
|
#include "chunk.h"
|
|
#include "protocol.h"
|
|
#include "test_utils.h"
|
|
#include "utils.h"
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
static void test_file_operations() {
|
|
char* test_path = "temp_file_test.txt";
|
|
char* test_content = "Hello, Chunk System!";
|
|
unsigned long long test_len = strlen(test_content);
|
|
|
|
file_write_to_disk(test_path, test_content, test_len, false, false);
|
|
|
|
File* f = file_create(test_path);
|
|
EXPECT_NOT_NULL(f);
|
|
EXPECT_EQ_STR(f->path, test_path);
|
|
EXPECT_NOT_NULL(f->data);
|
|
EXPECT_NULL(f->data->data);
|
|
EXPECT_EQ_INT((int)f->data->size, 0);
|
|
|
|
struct stat st;
|
|
stat(test_path, &st);
|
|
f->data->size = st.st_size;
|
|
|
|
file_load_data(f);
|
|
EXPECT_NOT_NULL(f->data);
|
|
EXPECT_NOT_NULL(f->data->data);
|
|
EXPECT_EQ_INT((int)f->data->size, (int)test_len);
|
|
EXPECT_EQ_INT(memcmp(f->data->data, test_content, test_len), 0);
|
|
|
|
file_destroy(f);
|
|
unlink(test_path);
|
|
}
|
|
|
|
static void test_chunk_operations() {
|
|
char* path1 = "temp_chunk_1.txt";
|
|
char* content1 = "chunk item 1";
|
|
unsigned long long len1 = strlen(content1);
|
|
|
|
char* path2 = "temp_chunk_2.txt";
|
|
char* content2 = "chunk item number 2";
|
|
unsigned long long len2 = strlen(content2);
|
|
|
|
file_write_to_disk(path1, content1, len1, false, false);
|
|
file_write_to_disk(path2, content2, len2, false, false);
|
|
|
|
struct stat st1, st2;
|
|
stat(path1, &st1);
|
|
stat(path2, &st2);
|
|
|
|
File* f1 = file_create(path1);
|
|
f1->data->size = st1.st_size;
|
|
File* f2 = file_create(path2);
|
|
f2->data->size = st2.st_size;
|
|
|
|
File* files[2] = {f1, f2};
|
|
Chunk* chunk = chunk_create(files, 2);
|
|
EXPECT_NOT_NULL(chunk);
|
|
EXPECT_EQ_INT(chunk->element_count, 2);
|
|
EXPECT_NOT_NULL(chunk->items[0]);
|
|
EXPECT_NOT_NULL(chunk->items[1]);
|
|
|
|
// load data before serializing
|
|
file_load_data(f1);
|
|
file_load_data(f2);
|
|
|
|
// Test chunk_serialize / chunk_deserialize round-trip
|
|
Data* serialized = chunk_serialize(chunk, false);
|
|
EXPECT_NOT_NULL(serialized);
|
|
|
|
Chunk* deserialized = chunk_deserialize(serialized, false);
|
|
EXPECT_NOT_NULL(deserialized);
|
|
EXPECT_EQ_INT(deserialized->element_count, 2);
|
|
EXPECT_EQ_STR(deserialized->items[0]->path, path1);
|
|
EXPECT_EQ_STR(deserialized->items[1]->path, path2);
|
|
EXPECT_EQ_INT((int)deserialized->items[0]->data->size, (int)len1);
|
|
EXPECT_EQ_INT((int)deserialized->items[1]->data->size, (int)len2);
|
|
EXPECT_EQ_INT(memcmp(deserialized->items[0]->data->data, content1, len1), 0);
|
|
EXPECT_EQ_INT(memcmp(deserialized->items[1]->data->data, content2, len2), 0);
|
|
|
|
data_destroy(serialized);
|
|
chunk_destroy(deserialized);
|
|
|
|
chunk_destroy(chunk);
|
|
|
|
unlink(path1);
|
|
unlink(path2);
|
|
}
|
|
|
|
/* A chunk mixing a regular file and an explicit directory entry (--dirs, with
|
|
* or without metadata) must round-trip through serialize/deserialize with the
|
|
* is_dir flag and the entry type marker preserved. */
|
|
static void test_chunk_dir_entry_roundtrip() {
|
|
const char* file_path = "temp_chunk_dir_file.txt";
|
|
const char* dir_path = "temp_chunk_dir_entry";
|
|
const char* content = "regular file payload";
|
|
|
|
/* A failed earlier run can leave artifacts behind; start clean. */
|
|
rmdir(dir_path);
|
|
unlink(file_path);
|
|
|
|
file_write_to_disk(file_path, content, strlen(content), false, false);
|
|
EXPECT_EQ_INT(mkdir(dir_path, 0755), 0);
|
|
|
|
for (int use_metadata = 0; use_metadata <= 1; use_metadata++) {
|
|
struct stat st;
|
|
EXPECT_EQ_INT(stat(file_path, &st), 0);
|
|
|
|
File* reg = file_create(file_path);
|
|
EXPECT_NOT_NULL(reg);
|
|
reg->data->size = (unsigned long long)st.st_size;
|
|
EXPECT_TRUE(file_load_data(reg));
|
|
|
|
File* dir = file_create(dir_path);
|
|
EXPECT_NOT_NULL(dir);
|
|
dir->is_dir = true;
|
|
|
|
if (use_metadata) {
|
|
reg->metadata = file_metadata_create(file_path, &st, false, false);
|
|
EXPECT_NOT_NULL(reg->metadata);
|
|
struct stat dst;
|
|
EXPECT_EQ_INT(stat(dir_path, &dst), 0);
|
|
dir->metadata = file_metadata_create(dir_path, &dst, false, false);
|
|
EXPECT_NOT_NULL(dir->metadata);
|
|
}
|
|
|
|
File* files[2] = {reg, dir};
|
|
Chunk* chunk = chunk_create(files, 2);
|
|
EXPECT_NOT_NULL(chunk);
|
|
|
|
Data* serialized = chunk_serialize(chunk, use_metadata != 0);
|
|
EXPECT_NOT_NULL(serialized);
|
|
Chunk* deserialized = chunk_deserialize(serialized, use_metadata != 0);
|
|
EXPECT_NOT_NULL(deserialized);
|
|
EXPECT_EQ_INT(deserialized->element_count, 2);
|
|
EXPECT_FALSE(deserialized->items[0]->is_dir);
|
|
EXPECT_EQ_STR(deserialized->items[0]->path, file_path);
|
|
EXPECT_EQ_INT((int)deserialized->items[0]->data->size, (int)strlen(content));
|
|
EXPECT_EQ_INT(memcmp(deserialized->items[0]->data->data, content, strlen(content)), 0);
|
|
EXPECT_TRUE(deserialized->items[1]->is_dir);
|
|
EXPECT_EQ_STR(deserialized->items[1]->path, dir_path);
|
|
EXPECT_EQ_INT((int)deserialized->items[1]->data->size, 0);
|
|
if (use_metadata) {
|
|
EXPECT_NOT_NULL(deserialized->items[0]->metadata);
|
|
EXPECT_NOT_NULL(deserialized->items[1]->metadata);
|
|
} else {
|
|
EXPECT_NULL(deserialized->items[0]->metadata);
|
|
EXPECT_NULL(deserialized->items[1]->metadata);
|
|
}
|
|
|
|
data_destroy(serialized);
|
|
chunk_destroy(deserialized);
|
|
chunk_destroy(chunk); /* frees reg and dir */
|
|
}
|
|
|
|
unlink(file_path);
|
|
rmdir(dir_path);
|
|
}
|
|
|
|
static void test_chunk_symlink_roundtrip() {
|
|
const char* file_path = "temp_chunk_symlink_file.txt";
|
|
const char* link_path = "temp_chunk_symlink";
|
|
const char* content = "regular payload";
|
|
const char* target = "temp_chunk_symlink_file.txt";
|
|
|
|
rmdir(link_path);
|
|
unlink(file_path);
|
|
|
|
file_write_to_disk(file_path, content, strlen(content), false, false);
|
|
|
|
for (int use_metadata = 0; use_metadata <= 1; use_metadata++) {
|
|
struct stat st;
|
|
EXPECT_EQ_INT(stat(file_path, &st), 0);
|
|
|
|
File* reg = file_create(file_path);
|
|
EXPECT_NOT_NULL(reg);
|
|
reg->data->size = (unsigned long long)st.st_size;
|
|
EXPECT_TRUE(file_load_data(reg));
|
|
|
|
File* link = file_create(link_path);
|
|
EXPECT_NOT_NULL(link);
|
|
link->is_symlink = true;
|
|
link->symlink_target = str_dup(target);
|
|
EXPECT_NOT_NULL(link->symlink_target);
|
|
|
|
if (use_metadata) {
|
|
reg->metadata = file_metadata_create(file_path, &st, false, false);
|
|
EXPECT_NOT_NULL(reg->metadata);
|
|
link->metadata = file_metadata_create(file_path, &st, false, false);
|
|
EXPECT_NOT_NULL(link->metadata);
|
|
}
|
|
|
|
File* files[2] = {reg, link};
|
|
Chunk* chunk = chunk_create(files, 2);
|
|
EXPECT_NOT_NULL(chunk);
|
|
|
|
Data* serialized = chunk_serialize(chunk, use_metadata != 0);
|
|
EXPECT_NOT_NULL(serialized);
|
|
Chunk* deserialized = chunk_deserialize(serialized, use_metadata != 0);
|
|
EXPECT_NOT_NULL(deserialized);
|
|
EXPECT_EQ_INT(deserialized->element_count, 2);
|
|
EXPECT_FALSE(deserialized->items[0]->is_symlink);
|
|
EXPECT_TRUE(deserialized->items[1]->is_symlink);
|
|
EXPECT_NULL(deserialized->items[0]->symlink_target);
|
|
EXPECT_EQ_STR(deserialized->items[1]->symlink_target, target);
|
|
EXPECT_EQ_INT((int)deserialized->items[1]->data->size, 0);
|
|
|
|
data_destroy(serialized);
|
|
chunk_destroy(deserialized);
|
|
chunk_destroy(chunk); /* frees reg and link */
|
|
}
|
|
|
|
unlink(file_path);
|
|
rmdir(link_path);
|
|
}
|
|
|
|
/* A --devices/--specials special entry (is_special + rdev) must round-trip
|
|
* through the chunk wire with a legal rdev. */
|
|
static void test_chunk_special_rdev_roundtrip() {
|
|
const char* path = "temp_chunk_special_node";
|
|
unlink(path);
|
|
File* special = file_create(path);
|
|
EXPECT_NOT_NULL(special);
|
|
special->is_special = true;
|
|
special->rdev_major = 1;
|
|
special->rdev_minor = 3;
|
|
struct stat st;
|
|
EXPECT_EQ_INT(stat("/dev/null", &st), 0);
|
|
special->metadata = file_metadata_create(path, &st, false, false);
|
|
EXPECT_NOT_NULL(special->metadata);
|
|
|
|
File* files[1] = {special};
|
|
Chunk* chunk = chunk_create(files, 1);
|
|
EXPECT_NOT_NULL(chunk);
|
|
Data* serialized = chunk_serialize(chunk, true);
|
|
EXPECT_NOT_NULL(serialized);
|
|
Chunk* deserialized = chunk_deserialize(serialized, true);
|
|
EXPECT_NOT_NULL(deserialized);
|
|
EXPECT_EQ_INT(deserialized->element_count, 1);
|
|
EXPECT_TRUE(deserialized->items[0]->is_special);
|
|
EXPECT_FALSE(deserialized->items[0]->is_dir);
|
|
EXPECT_EQ_INT((int)deserialized->items[0]->data->size, 0);
|
|
EXPECT_EQ_INT(deserialized->items[0]->rdev_major, 1);
|
|
EXPECT_EQ_INT(deserialized->items[0]->rdev_minor, 3);
|
|
EXPECT_NOT_NULL(deserialized->items[0]->metadata);
|
|
|
|
data_destroy(serialized);
|
|
chunk_destroy(deserialized);
|
|
chunk_destroy(chunk);
|
|
}
|
|
|
|
/* A special entry carrying an out-of-range rdev is a malformed chunk and must be
|
|
* rejected at deserialize (bounded by the same 0xffff / 0x00ffffff limits
|
|
* file_special_rdev_valid uses on the per-file wire), not deferred to the
|
|
* creation site. */
|
|
static void test_chunk_special_rdev_out_of_range_rejected() {
|
|
const char* path = "temp_chunk_special_bad_rdev";
|
|
unlink(path);
|
|
File* special = file_create(path);
|
|
EXPECT_NOT_NULL(special);
|
|
special->is_special = true;
|
|
special->rdev_major = 0x10000; /* > 0xffff */
|
|
special->rdev_minor = 3;
|
|
struct stat st;
|
|
EXPECT_EQ_INT(stat("/dev/null", &st), 0);
|
|
special->metadata = file_metadata_create(path, &st, false, false);
|
|
EXPECT_NOT_NULL(special->metadata);
|
|
|
|
File* files[1] = {special};
|
|
Chunk* chunk = chunk_create(files, 1);
|
|
EXPECT_NOT_NULL(chunk);
|
|
Data* serialized = chunk_serialize(chunk, true);
|
|
EXPECT_NOT_NULL(serialized);
|
|
const Chunk* deserialized = chunk_deserialize(serialized, true);
|
|
EXPECT_NULL(deserialized);
|
|
data_destroy(serialized);
|
|
chunk_destroy(chunk);
|
|
}
|
|
|
|
/* B6: chunk_deserialize() charges each retained per-file copy to the owning
|
|
* session's connection budget (MAX_CONNECTION_MEMORY) so queued chunk payloads
|
|
* are not held outside the per-connection ceiling; destroying the chunk returns
|
|
* the charge through the Data.owner path. */
|
|
static void test_chunk_deserialize_charges_session_budget() {
|
|
const char* path = "temp_chunk_charge.txt";
|
|
const char* content = "charge me to the connection budget";
|
|
unlink(path);
|
|
file_write_to_disk(path, content, strlen(content), false, false);
|
|
struct stat st;
|
|
EXPECT_EQ_INT(stat(path, &st), 0);
|
|
|
|
int p[2];
|
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
|
ProtocolSession session;
|
|
protocol_session_init(&session, p[0], p[1]);
|
|
protocol_session_set_max_alloc(&session, 4ULL * 1024 * 1024);
|
|
|
|
File* f = file_create(path);
|
|
EXPECT_NOT_NULL(f);
|
|
f->data->size = (unsigned long long)st.st_size;
|
|
EXPECT_TRUE(file_load_data(f));
|
|
File* files[1] = {f};
|
|
Chunk* chunk = chunk_create(files, 1);
|
|
EXPECT_NOT_NULL(chunk);
|
|
Data* serialized = chunk_serialize(chunk, false);
|
|
EXPECT_NOT_NULL(serialized);
|
|
/* Simulate a received buffer carrying its owning session. */
|
|
serialized->owner = &session;
|
|
|
|
Chunk* deserialized = chunk_deserialize(serialized, false);
|
|
EXPECT_NOT_NULL(deserialized);
|
|
unsigned long long charged = atomic_load(&session.total_allocated_bytes);
|
|
EXPECT_EQ_INT((int)charged, (int)strlen(content));
|
|
chunk_destroy(deserialized);
|
|
/* The copy's charge is released with the File/Data on destroy. */
|
|
EXPECT_EQ_INT((int)atomic_load(&session.total_allocated_bytes), 0);
|
|
|
|
data_destroy(serialized);
|
|
chunk_destroy(chunk);
|
|
close(p[0]);
|
|
close(p[1]);
|
|
unlink(path);
|
|
}
|
|
|
|
void test_chunk() {
|
|
test_file_operations();
|
|
test_chunk_operations();
|
|
test_chunk_dir_entry_roundtrip();
|
|
test_chunk_symlink_roundtrip();
|
|
test_chunk_special_rdev_roundtrip();
|
|
test_chunk_special_rdev_out_of_range_rejected();
|
|
test_chunk_deserialize_charges_session_budget();
|
|
}
|