fix(xattr): fake-super device round-trip, --devices continue-on-error, harden stat parse

This commit is contained in:
2026-09-22 23:05:40 +02:00
parent 0b40c47d6a
commit 47b1b9b915
12 changed files with 310 additions and 47 deletions
+104 -1
View File
@@ -9,7 +9,9 @@
#include "charset.h"
#include "utils.h"
#include "protocol.h"
#include "xattr.h"
#include "test_utils.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdlib.h>
@@ -17,6 +19,7 @@
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/wait.h>
#include <sys/xattr.h>
#include <time.h>
#include <unistd.h>
@@ -348,7 +351,8 @@ static void test_file_save_to_disk_temp_dir_confined() {
mkdir(root, 0755);
mkdir("test_temp_confine_tmp/scratch", 0755);
mkdir("test_temp_confine_tmp/abs_scratch", 0755);
EXPECT_NOT_NULL(realpath("test_temp_confine_tmp/abs_scratch", inside_abs));
if (!realpath("test_temp_confine_tmp/abs_scratch", inside_abs))
EXPECT_FAIL("realpath(abs_scratch) failed; inside_abs would be uninitialized");
mkdir(outside, 0755);
File* f = file_create("file.txt");
@@ -1327,6 +1331,103 @@ static void test_special_socket_recreated() {
rmdir(root);
}
/* --fake-super device round-trip (rsync parity): a char/block device must be
* materialized as a REGULAR empty file whose user.rsync.%stat records the real
* rdev -- never as an mknod'ed node -- even on a privileged receiver. This is
* the non-privileged unit counterpart to the setpriv integration test (which
* the PR gate excludes). */
static void test_fake_super_device_writes_regular_file_with_rdev() {
const char* root = "test_fake_super_dev_tmp";
const char* node = "test_fake_super_dev_tmp/cdev";
unlink(node);
rmdir(root);
EXPECT_EQ_INT(mkdir(root, 0700), 0);
Config* cfg = config_create();
EXPECT_NOT_NULL(cfg);
cfg->fake_super = true;
cfg->preserve_devices = true;
cfg->preserve_perms = true;
cfg->use_metadata = true;
cfg->use_xattrs = true;
FileMetadata meta;
memset(&meta, 0, sizeof(meta));
meta.mode = S_IFCHR | 0644;
File* f = file_create("cdev");
EXPECT_NOT_NULL(f);
f->is_special = true;
f->rdev_major = 1;
f->rdev_minor = 3;
f->metadata = &meta;
EXPECT_EQ_INT(file_save_to_disk_full(root, f, cfg), FILE_SAVE_WRITTEN);
struct stat st;
EXPECT_EQ_INT(lstat(node, &st), 0);
EXPECT_TRUE(S_ISREG(st.st_mode)); /* never a real device node */
EXPECT_EQ_INT((int)st.st_size, 0);
char value[128] = {0};
ssize_t got = getxattr(node, FAKESUPER_XATTR, value, sizeof(value) - 1);
EXPECT_TRUE(got > 0);
EXPECT_EQ_STR(value, "20644 1,3 0:0"); /* the REAL rdev, not 0,0 */
f->metadata = NULL;
file_destroy(f);
config_delete(cfg);
unlink(node);
rmdir(root);
}
/* A char/block device that mknodat() refuses (EPERM/EACCES on an unprivileged
* receiver) must be a PER-ENTRY failure -- FILE_SAVE_FAILED, which the receiver
* counts and continues past -- never the fatal FILE_SAVE_ERROR that aborts the
* stream. The unit suite normally runs as root, so drop the effective uid to
* make the kernel refusal deterministic. */
static void test_device_mknod_failure_is_per_entry() {
const char* root = "test_device_eperm_tmp";
const char* node = "test_device_eperm_tmp/cdev";
unlink(node);
rmdir(root);
EXPECT_EQ_INT(mkdir(root, 0777), 0);
Config* cfg = config_create();
EXPECT_NOT_NULL(cfg);
cfg->preserve_devices = true;
cfg->use_metadata = true;
FileMetadata meta;
memset(&meta, 0, sizeof(meta));
meta.mode = S_IFCHR | 0644;
File* f = file_create("cdev");
EXPECT_NOT_NULL(f);
f->is_special = true;
f->rdev_major = 1;
f->rdev_minor = 3;
f->metadata = &meta;
uid_t saved = geteuid();
bool dropped = false;
if (saved == 0 && seteuid(65534) == 0)
dropped = true;
FileSaveResult result = file_save_to_disk_full(root, f, cfg);
if (dropped)
EXPECT_EQ_INT(seteuid(saved), 0);
EXPECT_EQ_INT(result, FILE_SAVE_FAILED);
/* Nothing was created: no device node and no regular-file fallback. */
struct stat st;
EXPECT_EQ_INT(lstat(node, &st), -1);
f->metadata = NULL;
file_destroy(f);
config_delete(cfg);
rmdir(root);
}
static void test_inplace_overwrite_truncates_shorter_payload() {
const char* root = "test_inplace_trunc_tmp";
const char* path = "test_inplace_trunc_tmp/big.txt";
@@ -2408,6 +2509,8 @@ void test_file() {
test_new_file_mode_honors_source_and_umask();
test_special_fifo_mode_honors_source_and_umask();
test_special_socket_recreated();
test_fake_super_device_writes_regular_file_with_rdev();
test_device_mknod_failure_is_per_entry();
test_inplace_overwrite_truncates_shorter_payload();
test_inplace_refuses_fifo_destination();
test_inplace_refuses_device_destination();