fix(xattr): fake-super device round-trip, --devices continue-on-error, harden stat parse
This commit is contained in:
@@ -64,6 +64,13 @@ def test_read_batch_absolute_temp_dir_inside_root_accepted(tmp_path):
|
||||
clean_dir(dest)
|
||||
scratch = os.path.join(dest, "scratch")
|
||||
os.makedirs(scratch)
|
||||
# Stamp the scratch dir with an old mtime so the test can prove the receiver
|
||||
# really created (and then removed) its temp file there: the directory mtime
|
||||
# changes when an entry is created/removed, so a silently-ignored --temp-dir
|
||||
# would leave the stamp untouched. An empty scratch dir alone does not
|
||||
# distinguish "used and cleaned up" from "never used".
|
||||
stale_mtime = 946684800 # 2000-01-01
|
||||
os.utime(scratch, (stale_mtime, stale_mtime))
|
||||
|
||||
batch = _make_batch(str(tmp_path), source)
|
||||
result = _run(["--read-batch", batch, dest, "--temp-dir", scratch])
|
||||
@@ -73,6 +80,9 @@ def test_read_batch_absolute_temp_dir_inside_root_accepted(tmp_path):
|
||||
for rel, data in FILES.items():
|
||||
assert _read(os.path.join(received, rel)) == data, f"content mismatch for {rel}"
|
||||
assert os.listdir(scratch) == [], "scratch dir was not left clean"
|
||||
assert os.stat(scratch).st_mtime != stale_mtime, (
|
||||
"--temp-dir scratch dir was never written to (temp file not created there)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.ci
|
||||
@@ -98,6 +108,11 @@ def test_tcp_absolute_temp_dir_inside_root_accepted(shared_server):
|
||||
clean_dir(dest)
|
||||
scratch = os.path.join(dest, "scratch")
|
||||
os.makedirs(scratch)
|
||||
# See test_read_batch_absolute_temp_dir_inside_root_accepted: the stale
|
||||
# mtime makes actual scratch usage observable (the temp file creation and
|
||||
# removal bump the directory mtime).
|
||||
stale_mtime = 946684800 # 2000-01-01
|
||||
os.utime(scratch, (stale_mtime, stale_mtime))
|
||||
|
||||
result, _ = run_client(source, dest, flags=["--temp-dir", scratch], port=shared_server.port)
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)[:300]
|
||||
@@ -105,6 +120,9 @@ def test_tcp_absolute_temp_dir_inside_root_accepted(shared_server):
|
||||
for rel, data in FILES.items():
|
||||
assert _read(os.path.join(received, rel)) == data, f"content mismatch for {rel}"
|
||||
assert os.listdir(scratch) == [], "scratch dir was not left clean"
|
||||
assert os.stat(scratch).st_mtime != stale_mtime, (
|
||||
"--temp-dir scratch dir was never written to (temp file not created there)"
|
||||
)
|
||||
|
||||
|
||||
def test_tcp_absolute_temp_dir_outside_root_rejected(shared_server):
|
||||
|
||||
+104
-1
@@ -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();
|
||||
|
||||
@@ -450,6 +450,24 @@ static void test_fake_super_rsync_format() {
|
||||
EXPECT_EQ_INT((int)st.st_uid, (int)before.st_uid);
|
||||
EXPECT_EQ_INT((int)st.st_gid, (int)before.st_gid);
|
||||
|
||||
/* Hardened parser: an out-of-range field (previously UB via sscanf("%u")),
|
||||
a missing field, or trailing garbage is rejected cleanly instead of being
|
||||
silently accepted. */
|
||||
const char* malformed[] = {
|
||||
"20644 65536,3 111:222", /* major > 0xffff */
|
||||
"20644 1,16777216 111:222", /* minor > 0xffffff */
|
||||
"20644 1,3 111:222 trailing", /* trailing garbage */
|
||||
"20644 1,3 111", /* missing gid */
|
||||
"20644 1,3 4294967296:222", /* uid > UINT_MAX */
|
||||
"20644 1,3 111:4294967296", /* gid > UINT_MAX */
|
||||
"99999999999999999999 1,3 0:0", /* mode overflow */
|
||||
"", /* empty record */
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(malformed) / sizeof(malformed[0]); i++) {
|
||||
EXPECT_EQ_INT((int)fsetxattr(fd, FAKESUPER_XATTR, malformed[i], strlen(malformed[i]), 0), 0);
|
||||
EXPECT_FALSE(fake_super_restore_fd(fd, policy));
|
||||
}
|
||||
|
||||
close(fd);
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user