fix(receiver): confine --temp-dir scratch dir and gate setuid bits
Three receiver security fixes from the audit: 1. --temp-dir symlink escape (High): file_open_temp_dir() opened the client-controlled scratch dir with a bare open(), so a symlink planted under the receive root let a peer redirect receiver scratch files outside the authorized root. The opened dir is now judged by the REAL path of its fd (via /proc/self/fd), and any target outside the authorized receive root is refused with a logged error (EACCES). An in-root symlink (the EXDEV cross-filesystem fallback case) still works, and the no-root local batch path is unchanged. 2. setuid/setgid/sticky under SUPER_MODE_OFF (High): the special bits were applied under --perms (and via --chmod) even when the connection forbade super-user activities. FileAttrPolicy gains super_permitted, set by file_attr_policy_from_config() from privilege_super_mode_permitted(); metadata_mode_for_policy(), the symlink path, the special-node creation path, and the deferred directory-mode apply now strip the special bits when it is false. Exact rsync semantics are preserved when permitted. 3. daemon umask (Low): daemonize() forced umask(0), so implied parent directories created without -p were world-writable 0777. Set the conventional daemon umask 022 instead (rsync never forces 0); -p/-a mode preservation is unaffected because it restores modes via fchmod. Tests: new unit tests for file_open_temp_dir confinement and the masked/unmasked special-bit policy (incl. the --chmod path), a daemon world-writable-dir regression test, an integration escape test, and a root-only integration test asserting special bits are masked without --allow-super. The old cross-filesystem test encoded the vulnerable behavior (symlink target outside the root) and is replaced by the escape test; the EXDEV fallback code is retained for in-root links.
This commit is contained in:
@@ -332,6 +332,21 @@ class TestDaemonModuleSelection:
|
||||
assert not missing, f"missing: {missing[:5]}"
|
||||
assert not mismatches, f"mismatch: {mismatches[:5]}"
|
||||
|
||||
def test_daemon_new_dirs_not_world_writable(self, daemon):
|
||||
"""The daemon must not force umask 0: implied parent directories created
|
||||
without -p are the source default (0755 under a 022 umask), never
|
||||
world-writable 0777."""
|
||||
sub = os.path.join(FILES_MODULE, "umask_check")
|
||||
shutil.rmtree(sub, ignore_errors=True)
|
||||
os.makedirs(sub, exist_ok=True)
|
||||
result = _push("127.0.0.1::files/umask_check", daemon.port)
|
||||
assert result.returncode == 0, result.stderr or result.stdout
|
||||
received = get_dest_received_dir(sub, SOURCE_DIR)
|
||||
nested = os.path.join(received, "nested")
|
||||
assert os.path.isdir(nested), f"nested dir missing under {received}"
|
||||
mode = stat.S_IMODE(os.stat(nested).st_mode)
|
||||
assert (mode & 0o022) == 0, f"implied directory is group/other writable: {oct(mode)}"
|
||||
|
||||
|
||||
class TestDaemonRejection:
|
||||
def _tree_files(self):
|
||||
|
||||
@@ -2606,35 +2606,30 @@ class TestTimeoutAndAllocLimits:
|
||||
mismatches, missing = verify_transfer(source, received)
|
||||
assert not missing and not mismatches
|
||||
|
||||
def test_temp_dir_cross_filesystem_fallback(self, shared_server):
|
||||
"""A confined relative --temp-dir that resolves (via a symlink under the
|
||||
destination root) to another filesystem must fall back to a non-atomic
|
||||
copy instead of aborting (rsync parity). Skipped when no second
|
||||
filesystem is available."""
|
||||
shm = "/dev/shm"
|
||||
if not os.path.isdir(shm):
|
||||
pytest.skip("/dev/shm not available")
|
||||
if os.stat(shm).st_dev == os.stat(TEST_DATA_DIR).st_dev:
|
||||
pytest.skip("/dev/shm is on the same filesystem as the test data")
|
||||
scratch = os.path.join(shm, f"fastsync_tmp_{os.getpid()}")
|
||||
shutil.rmtree(scratch, ignore_errors=True)
|
||||
os.makedirs(scratch)
|
||||
def test_temp_dir_symlink_escape_rejected(self, shared_server):
|
||||
"""A symlink planted inside the destination root pointing outside it
|
||||
must not redirect receiver scratch files: --temp-dir=<that link> is
|
||||
refused and nothing is written at the link target. An in-root symlink
|
||||
(e.g. to a mount point that stays inside the authorized root) is still
|
||||
accepted, preserving the engine's EXDEV cross-filesystem fallback."""
|
||||
source, dest = self._seed("tempdir_escape_src")
|
||||
outside = "/tmp/fastsync_tempdir_escape_%d" % os.getpid()
|
||||
shutil.rmtree(outside, ignore_errors=True)
|
||||
os.makedirs(outside)
|
||||
link = os.path.join(dest, "escape_scratch")
|
||||
if os.path.lexists(link):
|
||||
os.unlink(link)
|
||||
os.symlink(outside, link)
|
||||
try:
|
||||
source, dest = self._seed("tempdir_xdev_src")
|
||||
# The receiver resolves a relative temp dir under the destination
|
||||
# root; a symlink there points the scratch at the second filesystem.
|
||||
link = os.path.join(dest, "xdev_scratch")
|
||||
os.symlink(scratch, link)
|
||||
result, _ = run_client(source, dest, flags=["--temp-dir", "xdev_scratch"],
|
||||
result, _ = run_client(source, dest, flags=["--temp-dir", "escape_scratch"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, f"cross-fs temp-dir failed: {result.stderr[:300]}"
|
||||
assert result.returncode != 0, "an escaping --temp-dir symlink must be refused"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
mismatches, missing = verify_transfer(source, received)
|
||||
assert not missing, f"Missing: {missing}"
|
||||
assert not mismatches, f"Mismatch: {mismatches}"
|
||||
assert os.listdir(scratch) == [], "temp files left behind in the cross-fs scratch"
|
||||
assert not os.path.exists(os.path.join(received, "f.txt")), \
|
||||
"the receiver must not fall back to writing the file"
|
||||
assert os.listdir(outside) == [], "receiver wrote outside the authorized root"
|
||||
finally:
|
||||
shutil.rmtree(scratch, ignore_errors=True)
|
||||
shutil.rmtree(outside, ignore_errors=True)
|
||||
|
||||
|
||||
class TestRemoteOptionTransport:
|
||||
@@ -5782,6 +5777,35 @@ class TestStandaloneSuperDefault:
|
||||
"standalone server accepted --copy-as without --allow-super"
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.geteuid() != 0,
|
||||
reason="root triggers the SUPER_MODE_OFF default and can create setuid sources",
|
||||
)
|
||||
def test_special_bits_masked_without_allow_super(self):
|
||||
"""A root standalone server without --allow-super forces SUPER_MODE_OFF,
|
||||
so client-supplied setuid/setgid/sticky bits must be stripped even under
|
||||
-p (they are super-user activities just like device-node creation)."""
|
||||
source = os.path.join(TEST_DATA_DIR, "super_default_mode_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "super_default_mode_dst")
|
||||
clean_dir(source)
|
||||
clean_dir(dest)
|
||||
src_file = os.path.join(source, "priv.sh")
|
||||
with open(src_file, "wb") as f:
|
||||
f.write(b"#!/bin/sh\necho hi\n")
|
||||
os.chmod(src_file, 0o4755)
|
||||
server = ServerManager()
|
||||
server.start() # deliberately no --allow-super -> SUPER_MODE_OFF as root
|
||||
try:
|
||||
result, _ = run_client(source, dest, flags=["-p"], port=server.port)
|
||||
finally:
|
||||
server.stop()
|
||||
assert result.returncode == 0, f"exit {result.returncode}: {(result.stderr or '')[:200]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
mode = stat.S_IMODE(os.stat(os.path.join(received, "priv.sh")).st_mode)
|
||||
assert (mode & (stat.S_ISUID | stat.S_ISGID | stat.S_ISVTX)) == 0, \
|
||||
f"--no-super receiver kept a privileged bit: {oct(mode)}"
|
||||
assert (mode & 0o777) == 0o755, f"ordinary permission bits lost: {oct(mode)}"
|
||||
|
||||
@pytest.mark.skipif(os.geteuid() != 0, reason="root can create the source device node")
|
||||
def test_devices_skipped_without_allow_super(self):
|
||||
"""Root standalone server without --allow-super must skip device-node
|
||||
|
||||
+81
-16
@@ -377,6 +377,70 @@ static void test_file_save_to_disk_temp_dir_confined() {
|
||||
rmdir(outside);
|
||||
}
|
||||
|
||||
/* A client-planted symlink under the receive root must never redirect the
|
||||
* --temp-dir scratch directory outside the authorized root: the REAL path of
|
||||
* the opened dir is checked. An in-root symlink (the EXDEV cross-filesystem
|
||||
* case) must still be accepted. */
|
||||
static void test_file_open_temp_dir_symlink_confinement() {
|
||||
const char* root = "test_tempdir_link_root";
|
||||
const char* outside = "test_tempdir_link_outside";
|
||||
char root_abs[PATH_MAX];
|
||||
char outside_abs[PATH_MAX];
|
||||
unlink("test_tempdir_link_root/escape");
|
||||
unlink("test_tempdir_link_root/inside_link");
|
||||
rmdir("test_tempdir_link_root/scratch");
|
||||
rmdir(root);
|
||||
rmdir(outside);
|
||||
EXPECT_EQ_INT(mkdir(root, 0755), 0);
|
||||
EXPECT_EQ_INT(mkdir(outside, 0755), 0);
|
||||
EXPECT_NOT_NULL(realpath(root, root_abs));
|
||||
EXPECT_NOT_NULL(realpath(outside, outside_abs));
|
||||
int root_fd = open(root_abs, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
EXPECT_TRUE(root_fd >= 0);
|
||||
// cppcheck-suppress knownConditionTrueFalse
|
||||
if (root_fd < 0) {
|
||||
rmdir(root);
|
||||
rmdir(outside);
|
||||
return;
|
||||
}
|
||||
EXPECT_TRUE(utils_set_authorized_root(root_fd, root_abs));
|
||||
|
||||
/* An existing in-root scratch dir opens normally. */
|
||||
char* scratch = path_cat(root_abs, "scratch");
|
||||
EXPECT_NOT_NULL(scratch);
|
||||
EXPECT_EQ_INT(mkdir(scratch, 0755), 0);
|
||||
int scratch_fd = file_open_temp_dir(scratch);
|
||||
EXPECT_TRUE(scratch_fd >= 0);
|
||||
if (scratch_fd >= 0)
|
||||
close(scratch_fd);
|
||||
|
||||
/* A symlink whose target is outside the root is refused. */
|
||||
char* escape = path_cat(root_abs, "escape");
|
||||
EXPECT_NOT_NULL(escape);
|
||||
EXPECT_EQ_INT(symlink(outside_abs, escape), 0);
|
||||
EXPECT_EQ_INT(file_open_temp_dir(escape), -1);
|
||||
|
||||
/* A symlink that stays inside the root is accepted (EXDEV fallback path). */
|
||||
char* inside_link = path_cat(root_abs, "inside_link");
|
||||
EXPECT_NOT_NULL(inside_link);
|
||||
EXPECT_EQ_INT(symlink(scratch, inside_link), 0);
|
||||
int link_fd = file_open_temp_dir(inside_link);
|
||||
EXPECT_TRUE(link_fd >= 0);
|
||||
if (link_fd >= 0)
|
||||
close(link_fd);
|
||||
|
||||
free(inside_link);
|
||||
free(escape);
|
||||
free(scratch);
|
||||
utils_set_authorized_root(-1, NULL);
|
||||
close(root_fd);
|
||||
unlink("test_tempdir_link_root/escape");
|
||||
unlink("test_tempdir_link_root/inside_link");
|
||||
rmdir("test_tempdir_link_root/scratch");
|
||||
rmdir(root);
|
||||
rmdir(outside);
|
||||
}
|
||||
|
||||
/* Issue #251: file_save_to_disk_full must distinguish receiver-side skips
|
||||
(--existing/--ignore-existing/--update) from real writes so the sender can
|
||||
decide whether --remove-source-files may unlink its source. */
|
||||
@@ -1040,8 +1104,8 @@ static void test_atomic_no_perms_preserves_destination_mode() {
|
||||
|
||||
/* No -p/-E: the pre-existing 0640 survives the atomic overwrite. */
|
||||
bool ok = file_to_disk_secure_attrs(path, "data", 4, false, false, false, &m,
|
||||
(FileAttrPolicy){false, false, false, false}, false, false,
|
||||
false, NULL, false, false, NULL);
|
||||
(FileAttrPolicy){false, false, false, false, true}, false,
|
||||
false, false, NULL, false, false, NULL);
|
||||
EXPECT_TRUE(ok);
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
@@ -1049,8 +1113,8 @@ static void test_atomic_no_perms_preserves_destination_mode() {
|
||||
|
||||
/* -p: the source mode wins. */
|
||||
ok = file_to_disk_secure_attrs(path, "data2", 5, false, false, false, &m,
|
||||
(FileAttrPolicy){true, true, false, false}, false, false, false,
|
||||
NULL, false, false, NULL);
|
||||
(FileAttrPolicy){true, true, false, false, true}, false, false,
|
||||
false, NULL, false, false, NULL);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
EXPECT_EQ_INT((int)(st.st_mode & 0777), 0755);
|
||||
@@ -1060,8 +1124,8 @@ static void test_atomic_no_perms_preserves_destination_mode() {
|
||||
source 0755 gives 0750, not 0751 and not the scratch 0711. */
|
||||
EXPECT_EQ_INT(chmod(path, 0640), 0);
|
||||
ok = file_to_disk_secure_attrs(path, "data3", 6, false, false, false, &m,
|
||||
(FileAttrPolicy){false, false, false, true}, false, false, false,
|
||||
NULL, false, false, NULL);
|
||||
(FileAttrPolicy){false, false, false, true, true}, false, false,
|
||||
false, NULL, false, false, NULL);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
EXPECT_EQ_INT((int)(st.st_mode & 0777), 0750);
|
||||
@@ -1073,8 +1137,8 @@ static void test_atomic_no_perms_preserves_destination_mode() {
|
||||
const char* fresh = "test_attr_split_fresh.txt";
|
||||
unlink(fresh);
|
||||
ok = file_to_disk_secure_attrs(fresh, "data", 4, false, false, false, &m,
|
||||
(FileAttrPolicy){false, false, false, false}, false, false, false,
|
||||
NULL, false, false, NULL);
|
||||
(FileAttrPolicy){false, false, false, false, true}, false, false,
|
||||
false, NULL, false, false, NULL);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_EQ_INT(stat(fresh, &st), 0);
|
||||
EXPECT_EQ_INT((int)(st.st_mode & 0777), (int)(m.mode & 0777 & ~(mode_t)file_process_umask()));
|
||||
@@ -1083,8 +1147,8 @@ static void test_atomic_no_perms_preserves_destination_mode() {
|
||||
/* Without any metadata the historical fixed 0644 default still applies. */
|
||||
unlink(fresh);
|
||||
ok = file_to_disk_secure_attrs(fresh, "data", 4, false, false, false, NULL,
|
||||
(FileAttrPolicy){false, false, false, false}, false, false, false,
|
||||
NULL, false, false, NULL);
|
||||
(FileAttrPolicy){false, false, false, false, true}, false, false,
|
||||
false, NULL, false, false, NULL);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_EQ_INT(stat(fresh, &st), 0);
|
||||
EXPECT_EQ_INT((int)(st.st_mode & 0777), 0644);
|
||||
@@ -1104,8 +1168,8 @@ static void test_new_file_mode_honors_source_and_umask() {
|
||||
m.gid = getegid();
|
||||
|
||||
bool ok = file_to_disk_secure_attrs(path, "x", 1, false, false, false, &m,
|
||||
(FileAttrPolicy){false, false, false, false}, false, false,
|
||||
false, NULL, false, false, NULL);
|
||||
(FileAttrPolicy){false, false, false, false, true}, false,
|
||||
false, false, NULL, false, false, NULL);
|
||||
EXPECT_TRUE(ok);
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
@@ -1663,8 +1727,8 @@ static void test_file_write_to_disk_partial_retention() {
|
||||
m.atime_valid = false;
|
||||
m.crtime_valid = false;
|
||||
bool ok = file_to_disk_secure_attrs(path, content, strlen(content), false, false, true, &m,
|
||||
(FileAttrPolicy){true, true, false, false}, false, false,
|
||||
false, NULL, false, true, NULL);
|
||||
(FileAttrPolicy){true, true, false, false, true}, false,
|
||||
false, false, NULL, false, true, NULL);
|
||||
EXPECT_FALSE(ok); /* the write itself succeeded, but metadata restore failed */
|
||||
/* Retained: the already-written temp now sits at the destination path. */
|
||||
int fd = open(path, O_RDONLY);
|
||||
@@ -1683,8 +1747,8 @@ static void test_file_write_to_disk_partial_retention() {
|
||||
|
||||
/* Same failure with keep_partial=false: temp is unlinked, nothing retained. */
|
||||
ok = file_to_disk_secure_attrs(path, content, strlen(content), false, false, true, &m,
|
||||
(FileAttrPolicy){true, true, false, false}, false, false, false,
|
||||
NULL, false, false, NULL);
|
||||
(FileAttrPolicy){true, true, false, false, true}, false, false,
|
||||
false, NULL, false, false, NULL);
|
||||
EXPECT_FALSE(ok);
|
||||
EXPECT_TRUE(access(path, F_OK) == -1);
|
||||
}
|
||||
@@ -2264,6 +2328,7 @@ void test_file() {
|
||||
test_file_save_to_disk_ignore_existing_entry_types();
|
||||
test_file_save_to_disk_partial_install();
|
||||
test_file_save_to_disk_temp_dir_confined();
|
||||
test_file_open_temp_dir_symlink_confinement();
|
||||
test_file_save_to_disk_reports_skips();
|
||||
test_file_write_to_disk_sparse_preserves_holes();
|
||||
test_file_write_to_disk_partial_retention();
|
||||
|
||||
+66
-32
@@ -339,7 +339,7 @@ static void test_file_restore_metadata_applies_atime() {
|
||||
m.crtime_sec = 0;
|
||||
m.crtime_nsec = 0;
|
||||
|
||||
file_restore_metadata(path, &m, (FileAttrPolicy){true, true, true, false});
|
||||
file_restore_metadata(path, &m, (FileAttrPolicy){true, true, true, false, true});
|
||||
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
@@ -378,7 +378,7 @@ static void test_file_restore_metadata() {
|
||||
.atime_valid = false,
|
||||
.crtime_valid = false};
|
||||
|
||||
file_restore_metadata(path, &m, (FileAttrPolicy){true, true, false, false});
|
||||
file_restore_metadata(path, &m, (FileAttrPolicy){true, true, false, false, true});
|
||||
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
@@ -395,7 +395,7 @@ static void test_file_restore_executability_only() {
|
||||
|
||||
FileMetadata m = {
|
||||
.mode = 0751, .uid = getuid(), .gid = getgid(), .mtime_sec = 0, .mtime_nsec = 0};
|
||||
file_restore_metadata(path, &m, (FileAttrPolicy){false, false, false, true});
|
||||
file_restore_metadata(path, &m, (FileAttrPolicy){false, false, false, true, true});
|
||||
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
@@ -409,7 +409,7 @@ static void test_directory_restore_executability_only() {
|
||||
|
||||
FileMetadata m = {
|
||||
.mode = 0755, .uid = getuid(), .gid = getgid(), .mtime_sec = 0, .mtime_nsec = 0};
|
||||
file_restore_metadata(path, &m, (FileAttrPolicy){false, false, false, true});
|
||||
file_restore_metadata(path, &m, (FileAttrPolicy){false, false, false, true, true});
|
||||
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
@@ -437,7 +437,7 @@ static void test_file_restore_executability_rsync_rule() {
|
||||
EXPECT_TRUE(file_write_to_disk(path, "x", 1, false, false));
|
||||
EXPECT_EQ_INT(chmod(path, cases[i].dest), 0);
|
||||
FileMetadata m = {.mode = cases[i].src, .uid = getuid(), .gid = getgid()};
|
||||
file_restore_metadata(path, &m, (FileAttrPolicy){false, false, false, true});
|
||||
file_restore_metadata(path, &m, (FileAttrPolicy){false, false, false, true, true});
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
EXPECT_EQ_INT(st.st_mode & 0777, cases[i].want);
|
||||
@@ -453,34 +453,60 @@ static void test_file_restore_executability_rsync_rule() {
|
||||
* bits from the destination and --perms wins when both are set. */
|
||||
static void test_metadata_mode_for_policy() {
|
||||
mode_t out = 0xdead;
|
||||
EXPECT_FALSE(
|
||||
metadata_mode_for_policy(0777, 0644, (FileAttrPolicy){false, false, false, false}, &out));
|
||||
EXPECT_FALSE(metadata_mode_for_policy(0777, 0644,
|
||||
(FileAttrPolicy){false, false, false, false, true}, &out));
|
||||
EXPECT_EQ_INT((int)out, 0xdead); /* untouched when no change is requested */
|
||||
|
||||
EXPECT_TRUE(
|
||||
metadata_mode_for_policy(0777, 0644, (FileAttrPolicy){true, false, false, false}, &out));
|
||||
EXPECT_TRUE(metadata_mode_for_policy(0777, 0644,
|
||||
(FileAttrPolicy){true, false, false, false, true}, &out));
|
||||
EXPECT_EQ_INT((int)(out & 0777), 0777); /* group/other write is preserved */
|
||||
|
||||
mode_t specials = (mode_t)(S_ISUID | S_ISGID | S_ISVTX | 0672);
|
||||
EXPECT_TRUE(
|
||||
metadata_mode_for_policy(specials, 0644, (FileAttrPolicy){true, false, false, false}, &out));
|
||||
EXPECT_TRUE(metadata_mode_for_policy(specials, 0644,
|
||||
(FileAttrPolicy){true, false, false, false, true}, &out));
|
||||
EXPECT_EQ_INT((int)(out & (S_ISUID | S_ISGID | S_ISVTX | 0777)),
|
||||
(int)(S_ISUID | S_ISGID | S_ISVTX | 0672));
|
||||
|
||||
/* SUPER_MODE_OFF: the special bits are stripped even under -p (this also
|
||||
* covers bits introduced by --chmod, whose result is fed in as source_mode),
|
||||
* while the ordinary permission bits are still copied. */
|
||||
EXPECT_TRUE(metadata_mode_for_policy(specials, 0644,
|
||||
(FileAttrPolicy){true, false, false, false, false}, &out));
|
||||
EXPECT_EQ_INT((int)(out & (S_ISUID | S_ISGID | S_ISVTX)), 0);
|
||||
EXPECT_EQ_INT((int)(out & 0777), 0672);
|
||||
EXPECT_TRUE(metadata_mode_for_policy(04755, 0644,
|
||||
(FileAttrPolicy){true, false, false, false, false}, &out));
|
||||
EXPECT_EQ_INT((int)(out & 07777), 0755);
|
||||
/* The same holds for a special bit introduced by --chmod=+s. */
|
||||
mode_t chmodded = 0;
|
||||
EXPECT_TRUE(chmod_apply(0755, "u+s", &chmodded));
|
||||
EXPECT_TRUE(metadata_mode_for_policy(chmodded, 0644,
|
||||
(FileAttrPolicy){true, false, false, false, false}, &out));
|
||||
EXPECT_EQ_INT((int)(out & 07777), 0755);
|
||||
|
||||
/* -E: a destination's own special bits survive unless super-user activities
|
||||
* are forbidden, in which case they are stripped from the derived base. */
|
||||
EXPECT_TRUE(metadata_mode_for_policy(0755, (mode_t)(S_ISUID | 0750),
|
||||
(FileAttrPolicy){false, false, false, true, true}, &out));
|
||||
EXPECT_EQ_INT((int)(out & (S_ISUID | 0777)), (int)(S_ISUID | 0750));
|
||||
EXPECT_TRUE(metadata_mode_for_policy(0755, (mode_t)(S_ISUID | 0750),
|
||||
(FileAttrPolicy){false, false, false, true, false}, &out));
|
||||
EXPECT_EQ_INT((int)(out & (S_ISUID | 0777)), 0750);
|
||||
|
||||
/* -E: exec bits derive from the DESTINATION's read bits. */
|
||||
EXPECT_TRUE(
|
||||
metadata_mode_for_policy(0755, 0644, (FileAttrPolicy){false, false, false, true}, &out));
|
||||
EXPECT_TRUE(metadata_mode_for_policy(0755, 0644,
|
||||
(FileAttrPolicy){false, false, false, true, true}, &out));
|
||||
EXPECT_EQ_INT((int)(out & 0777), 0755);
|
||||
EXPECT_TRUE(
|
||||
metadata_mode_for_policy(0644, 0755, (FileAttrPolicy){false, false, false, true}, &out));
|
||||
EXPECT_TRUE(metadata_mode_for_policy(0644, 0755,
|
||||
(FileAttrPolicy){false, false, false, true, true}, &out));
|
||||
EXPECT_EQ_INT((int)(out & 0777), 0644);
|
||||
EXPECT_TRUE(
|
||||
metadata_mode_for_policy(0755, 0600, (FileAttrPolicy){false, false, false, true}, &out));
|
||||
EXPECT_TRUE(metadata_mode_for_policy(0755, 0600,
|
||||
(FileAttrPolicy){false, false, false, true, true}, &out));
|
||||
EXPECT_EQ_INT((int)(out & 0777), 0700);
|
||||
|
||||
/* --perms wins over -E when both are set. */
|
||||
EXPECT_TRUE(
|
||||
metadata_mode_for_policy(0700, 0644, (FileAttrPolicy){true, false, false, true}, &out));
|
||||
metadata_mode_for_policy(0700, 0644, (FileAttrPolicy){true, false, false, true, true}, &out));
|
||||
EXPECT_EQ_INT((int)(out & 0777), 0700);
|
||||
}
|
||||
|
||||
@@ -493,8 +519,8 @@ static void test_new_file_mode_from_source_and_umask() {
|
||||
mode_t want = (mode_t)(0751 & 0777 & ~(mode_t)file_process_umask());
|
||||
|
||||
bool ok = file_to_disk_secure_attrs(path, "x", 1, false, false, false, &m,
|
||||
(FileAttrPolicy){false, false, false, false}, false, false,
|
||||
false, NULL, false, false, NULL);
|
||||
(FileAttrPolicy){false, false, false, false, true}, false,
|
||||
false, false, NULL, false, false, NULL);
|
||||
EXPECT_TRUE(ok);
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
@@ -503,8 +529,8 @@ static void test_new_file_mode_from_source_and_umask() {
|
||||
|
||||
/* -E on top of the source&~umask base (src 0751, umask 022 -> 0751). */
|
||||
ok = file_to_disk_secure_attrs(path, "x", 1, false, false, false, &m,
|
||||
(FileAttrPolicy){false, false, false, true}, false, false, false,
|
||||
NULL, false, false, NULL);
|
||||
(FileAttrPolicy){false, false, false, true, true}, false, false,
|
||||
false, NULL, false, false, NULL);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
mode_t want_e =
|
||||
@@ -529,7 +555,7 @@ static void test_file_restore_attribute_split() {
|
||||
.crtime_valid = false};
|
||||
|
||||
/* times only: mtime changes, mode stays 0640. */
|
||||
file_restore_metadata(path, &m, (FileAttrPolicy){false, true, false, false});
|
||||
file_restore_metadata(path, &m, (FileAttrPolicy){false, true, false, false, true});
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
EXPECT_EQ_INT(st.st_mode & 0777, 0640);
|
||||
@@ -543,7 +569,7 @@ static void test_file_restore_attribute_split() {
|
||||
FileMetadata m2 = m;
|
||||
m2.mode = 0700;
|
||||
m2.mtime_sec = 1600000000;
|
||||
file_restore_metadata(path, &m2, (FileAttrPolicy){false, false, false, false});
|
||||
file_restore_metadata(path, &m2, (FileAttrPolicy){false, false, false, false, true});
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
EXPECT_EQ_INT(st.st_mode & 0777, 0640);
|
||||
EXPECT_EQ_INT((int)st.st_mtime, 1000000000);
|
||||
@@ -569,6 +595,11 @@ static void test_file_attr_policy_from_config() {
|
||||
EXPECT_TRUE(p.times);
|
||||
EXPECT_TRUE(p.atimes);
|
||||
EXPECT_TRUE(p.executability);
|
||||
/* Default super mode (AUTO) permits special bits. */
|
||||
EXPECT_TRUE(p.super_permitted);
|
||||
c->super_mode = SUPER_MODE_OFF;
|
||||
p = file_attr_policy_from_config(c);
|
||||
EXPECT_FALSE(p.super_permitted);
|
||||
config_delete(c);
|
||||
}
|
||||
|
||||
@@ -582,8 +613,8 @@ static void test_perms_preserves_special_bits() {
|
||||
.mode = (mode_t)(S_ISUID | S_ISGID | S_ISVTX | 0755), .uid = getuid(), .gid = getgid()};
|
||||
|
||||
bool ok = file_to_disk_secure_attrs(path, "x", 1, false, false, false, &m,
|
||||
(FileAttrPolicy){true, false, false, false}, false, false,
|
||||
false, NULL, false, false, NULL);
|
||||
(FileAttrPolicy){true, false, false, false, true}, false,
|
||||
false, false, NULL, false, false, NULL);
|
||||
EXPECT_TRUE(ok);
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
@@ -670,7 +701,8 @@ static void test_file_restore_symlink_metadata() {
|
||||
|
||||
/* Positive path: a non-omitted apply stamps the link's own mtime. */
|
||||
FileMetadata applied = {.mtime_sec = 1000000000, .mtime_nsec = 0};
|
||||
file_restore_symlink_metadata(link, &applied, (FileAttrPolicy){false, true, false, false}, false);
|
||||
file_restore_symlink_metadata(link, &applied, (FileAttrPolicy){false, true, false, false, true},
|
||||
false);
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(lstat(link, &st), 0);
|
||||
EXPECT_TRUE(S_ISLNK(st.st_mode));
|
||||
@@ -679,7 +711,8 @@ static void test_file_restore_symlink_metadata() {
|
||||
|
||||
/* -J: a different time must be left untouched. */
|
||||
FileMetadata newer = {.mtime_sec = 1234567890, .mtime_nsec = 0};
|
||||
file_restore_symlink_metadata(link, &newer, (FileAttrPolicy){false, true, false, false}, true);
|
||||
file_restore_symlink_metadata(link, &newer, (FileAttrPolicy){false, true, false, false, true},
|
||||
true);
|
||||
EXPECT_EQ_INT(lstat(link, &st), 0);
|
||||
EXPECT_EQ_INT((int)st.st_mtime, (int)t1);
|
||||
if (symlink_times_supported)
|
||||
@@ -723,14 +756,14 @@ static void test_file_restore_metadata_fd_attribute_split() {
|
||||
|
||||
/* perms-only: mode applied, mtime untouched. */
|
||||
EXPECT_EQ_INT(fstat(fd, &before), 0);
|
||||
EXPECT_TRUE(file_restore_metadata_fd(fd, &m, (FileAttrPolicy){true, false, false, false}));
|
||||
EXPECT_TRUE(file_restore_metadata_fd(fd, &m, (FileAttrPolicy){true, false, false, false, true}));
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT(st.st_mode & 0777, 0755);
|
||||
EXPECT_EQ_INT((int)st.st_mtime, (int)before.st_mtime);
|
||||
|
||||
/* times-only: mtime applied, mode untouched. */
|
||||
EXPECT_EQ_INT(chmod(path, 0600), 0);
|
||||
EXPECT_TRUE(file_restore_metadata_fd(fd, &m, (FileAttrPolicy){false, true, false, false}));
|
||||
EXPECT_TRUE(file_restore_metadata_fd(fd, &m, (FileAttrPolicy){false, true, false, false, true}));
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT(st.st_mode & 0777, 0600);
|
||||
EXPECT_EQ_INT((int)st.st_mtime, 1234567890);
|
||||
@@ -740,7 +773,7 @@ static void test_file_restore_metadata_fd_attribute_split() {
|
||||
{.tv_sec = 1000000000, .tv_nsec = 0}};
|
||||
EXPECT_EQ_INT(futimens(fd, reset), 0);
|
||||
EXPECT_EQ_INT(fstat(fd, &before), 0);
|
||||
EXPECT_TRUE(file_restore_metadata_fd(fd, &m, (FileAttrPolicy){false, false, true, false}));
|
||||
EXPECT_TRUE(file_restore_metadata_fd(fd, &m, (FileAttrPolicy){false, false, true, false, true}));
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT((int)st.st_atime, 999999999);
|
||||
EXPECT_EQ_INT((int)st.st_mtime, (int)before.st_mtime);
|
||||
@@ -753,7 +786,8 @@ static void test_file_restore_metadata_fd_attribute_split() {
|
||||
m2.mode = 0700;
|
||||
m2.mtime_sec = 1600000000;
|
||||
m2.atime_sec = 1700000000;
|
||||
EXPECT_TRUE(file_restore_metadata_fd(fd, &m2, (FileAttrPolicy){false, false, false, false}));
|
||||
EXPECT_TRUE(
|
||||
file_restore_metadata_fd(fd, &m2, (FileAttrPolicy){false, false, false, false, true}));
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT(st.st_mode & 0777, 0640);
|
||||
EXPECT_EQ_INT((int)st.st_mtime, 1000000000);
|
||||
|
||||
+3
-3
@@ -248,7 +248,7 @@ static void test_link_copy_fallback_preserves_xattrs() {
|
||||
m.crtime_valid = false;
|
||||
|
||||
bool ok = file_to_disk_secure_link_attrs(dest, basis_dir, "payload", 7, false, &m,
|
||||
(FileAttrPolicy){true, true, false, false}, false,
|
||||
(FileAttrPolicy){true, true, false, false, true}, false,
|
||||
xattrs, true, NULL);
|
||||
xattr_list_free(xattrs);
|
||||
EXPECT_TRUE(ok);
|
||||
@@ -369,7 +369,7 @@ static void test_fake_super_restore() {
|
||||
}
|
||||
|
||||
/* No xattr present yet: restore is a silent no-op (returns false, no crash). */
|
||||
FileAttrPolicy policy = {true, true, false, false};
|
||||
FileAttrPolicy policy = {true, true, false, false, true};
|
||||
EXPECT_FALSE(fake_super_restore_fd(fd, policy));
|
||||
|
||||
fake_super_store_fd(fd, 1001, 1002, 0751, 1700000000, 123456789);
|
||||
@@ -423,7 +423,7 @@ static void test_fake_super_no_real_chown() {
|
||||
fake_super_store_fd(fd, 12345, 12346, 0755, 1700000000, 0);
|
||||
|
||||
Config* c = config_create();
|
||||
FileAttrPolicy policy = {true, true, false, false};
|
||||
FileAttrPolicy policy = {true, true, false, false, true};
|
||||
EXPECT_NOT_NULL(c);
|
||||
/* The strongest ownership request available plus permitted super mode. */
|
||||
c->preserve_owner = true;
|
||||
|
||||
Reference in New Issue
Block a user