Merge feat/p4-acl-xattr: -X/-A/--fake-super
# Conflicts: # src/shared/config.c # src/shared/file.c # src/shared/file_types.h # tests/integration/test_features.py # tests/test_client_cli.c # tests/test_config.c
This commit is contained in:
@@ -4432,3 +4432,175 @@ class TestSymlinkTrust:
|
||||
prefixed = os.path.join(received, "prefixed")
|
||||
assert os.path.islink(prefixed)
|
||||
assert os.readlink(prefixed) == "#SYMLINK/realfile.txt"
|
||||
def _xattr_supported(path):
|
||||
"""True when the filesystem hosting `path` supports user xattrs."""
|
||||
try:
|
||||
os.setxattr(path, "user.fastsync-probe", b"p")
|
||||
os.removexattr(path, "user.fastsync-probe")
|
||||
return True
|
||||
except (OSError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
class TestExtendedAttributes:
|
||||
"""-X/--xattrs, -A/--acls, --fake-super: portable extended metadata.
|
||||
|
||||
Runs unprivileged (CI is non-root). Everything is best-effort and guarded:
|
||||
a filesystem without xattr support, or an ACL toolchain/POSIX-ACL
|
||||
filesystem feature that is missing, is skipped rather than failed. The
|
||||
security boundary (only user.* and the system.posix_acl_* namespaces are
|
||||
ever applied) is asserted alongside the happy path."""
|
||||
|
||||
def _source_and_dest(self, name):
|
||||
source = os.path.join(TEST_DATA_DIR, name + "_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, name + "_dst")
|
||||
clean_dir(source)
|
||||
clean_dir(dest)
|
||||
return source, dest
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_xattrs_preserves_user_namespace(self, shared_server):
|
||||
source, dest = self._source_and_dest("xattr")
|
||||
f = os.path.join(source, "data.txt")
|
||||
with open(f, "wb") as fh:
|
||||
fh.write(b"xattr payload\n")
|
||||
if not _xattr_supported(f):
|
||||
pytest.skip("filesystem does not support user xattrs")
|
||||
os.setxattr(f, "user.foo", b"preserved-value")
|
||||
|
||||
result, _ = run_client(source, dest, flags=["-X"], port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-X sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert os.getxattr(os.path.join(received, "data.txt"), "user.foo") == b"preserved-value"
|
||||
|
||||
def test_without_xattrs_does_not_carry(self, shared_server):
|
||||
source, dest = self._source_and_dest("xattr_ctrl")
|
||||
f = os.path.join(source, "data.txt")
|
||||
with open(f, "wb") as fh:
|
||||
fh.write(b"plain\n")
|
||||
if not _xattr_supported(f):
|
||||
pytest.skip("filesystem does not support user xattrs")
|
||||
os.setxattr(f, "user.foo", b"must-not-travel")
|
||||
|
||||
result, _ = run_client(source, dest, port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"control sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
with pytest.raises(OSError):
|
||||
os.getxattr(os.path.join(received, "data.txt"), "user.foo")
|
||||
|
||||
def test_reserved_fake_super_key_not_forwarded(self, shared_server):
|
||||
"""A source file that already carries the reserved user.fastsync.stat
|
||||
record must NOT have it planted on the receiver during a plain -X run
|
||||
(it is receiver-only, so it cannot be spoofed for a later privileged
|
||||
restore)."""
|
||||
source, dest = self._source_and_dest("xattr_reserved")
|
||||
f = os.path.join(source, "data.txt")
|
||||
with open(f, "wb") as fh:
|
||||
fh.write(b"reserved\n")
|
||||
if not _xattr_supported(f):
|
||||
pytest.skip("filesystem does not support user xattrs")
|
||||
os.setxattr(f, "user.fastsync.stat", b"0:0:644:0:0")
|
||||
# A normal user.* attr still travels alongside.
|
||||
os.setxattr(f, "user.keep", b"yes")
|
||||
|
||||
result, _ = run_client(source, dest, flags=["-X"], port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-X reserved-key sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert os.getxattr(os.path.join(received, "data.txt"), "user.keep") == b"yes"
|
||||
with pytest.raises(OSError):
|
||||
os.getxattr(os.path.join(received, "data.txt"), "user.fastsync.stat")
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_xattrs_multithreaded(self, shared_server):
|
||||
source, dest = self._source_and_dest("xattr_mt")
|
||||
f = os.path.join(source, "data.txt")
|
||||
with open(f, "wb") as fh:
|
||||
fh.write(b"mt xattr\n")
|
||||
if not _xattr_supported(f):
|
||||
pytest.skip("filesystem does not support user xattrs")
|
||||
os.setxattr(f, "user.k", b"v")
|
||||
result, _ = run_client(source, dest, flags=["-X", "-m"], port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-X -m sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert os.getxattr(os.path.join(received, "data.txt"), "user.k") == b"v"
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_acls_via_posix_acl_xattr(self, shared_server):
|
||||
source, dest = self._source_and_dest("acl")
|
||||
f = os.path.join(source, "data.txt")
|
||||
with open(f, "wb") as fh:
|
||||
fh.write(b"acl payload\n")
|
||||
if not _xattr_supported(f):
|
||||
pytest.skip("filesystem does not support xattrs")
|
||||
acl_blob = None
|
||||
if shutil.which("setfacl") is not None:
|
||||
acl = subprocess.run(["setfacl", "-m", "o::r", f], capture_output=True, text=True)
|
||||
if acl.returncode == 0:
|
||||
try:
|
||||
acl_blob = os.getxattr(f, "system.posix_acl_access")
|
||||
except OSError:
|
||||
acl_blob = None
|
||||
if acl_blob is None:
|
||||
# No setfacl (common in the minimal CI image): synthesize a valid
|
||||
# non-trivial POSIX ACL ("u:current-uid:r") xattr blob directly.
|
||||
import struct
|
||||
try:
|
||||
uid_for_acl = os.geteuid() if os.geteuid() != 0 else 65534
|
||||
struct_entry = struct.pack("<HHI", 0x01, 0x4, 0xFFFFFFFF) # USER_OBJ r
|
||||
struct_entry += struct.pack("<HHI", 0x02, 0x4, uid_for_acl) # USER r
|
||||
struct_entry += struct.pack("<HHI", 0x04, 0x4, 0xFFFFFFFF) # GROUP_OBJ r
|
||||
struct_entry += struct.pack("<HHI", 0x10, 0x4, 0xFFFFFFFF) # MASK r
|
||||
struct_entry += struct.pack("<HHI", 0x20, 0x0, 0xFFFFFFFF) # OTHER ---
|
||||
blob = struct.pack("<I", 2) + struct_entry
|
||||
os.setxattr(f, "system.posix_acl_access", blob)
|
||||
acl_blob = os.getxattr(f, "system.posix_acl_access")
|
||||
except (OSError, struct.error) as e:
|
||||
pytest.skip(f"cannot set a POSIX ACL unprivileged: {e}")
|
||||
|
||||
result, _ = run_client(source, dest, flags=["-A"], port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-A sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert os.getxattr(os.path.join(received, "data.txt"),
|
||||
"system.posix_acl_access") == acl_blob
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_acls_imply_xattr_transport(self, shared_server):
|
||||
"""-A and -X enable the shared xattr transport; both attributes travel
|
||||
together, and a security.* attribute a malicious peer would send is
|
||||
never applied (receiver whitelist)."""
|
||||
source, dest = self._source_and_dest("acl_xattr")
|
||||
f = os.path.join(source, "data.txt")
|
||||
with open(f, "wb") as fh:
|
||||
fh.write(b"combined\n")
|
||||
if not _xattr_supported(f):
|
||||
pytest.skip("filesystem does not support xattrs")
|
||||
os.setxattr(f, "user.for-acl-flag", b"yes")
|
||||
result, _ = run_client(source, dest, flags=["-A", "-X"], port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-A -X sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert os.getxattr(os.path.join(received, "data.txt"), "user.for-acl-flag") == b"yes"
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_fake_super_stores_source_stat(self, shared_server):
|
||||
source, dest = self._source_and_dest("fakesuper")
|
||||
f = os.path.join(source, "data.txt")
|
||||
with open(f, "wb") as fh:
|
||||
fh.write(b"fake-super\n")
|
||||
if not _xattr_supported(f):
|
||||
pytest.skip("filesystem does not support xattrs")
|
||||
uid = os.stat(f).st_uid
|
||||
|
||||
result, _ = run_client(source, dest, flags=["--fake-super"], port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--fake-super sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
record = os.getxattr(os.path.join(received, "data.txt"), "user.fastsync.stat").decode()
|
||||
fields = record.split(":")
|
||||
assert len(fields) == 5
|
||||
assert fields[0] == str(uid), f"reserved uid field {fields[0]} != source uid {uid}"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "test_transport_ssh.h"
|
||||
#include "test_transport_tls.h"
|
||||
#include "test_utils.h"
|
||||
#include "test_xattr.h"
|
||||
#include <stdio.h>
|
||||
#include <signal.h>
|
||||
|
||||
@@ -67,6 +68,7 @@ int main() {
|
||||
RUN_TEST(test_client_cli);
|
||||
RUN_TEST(test_server);
|
||||
RUN_TEST(test_fuzz_smoke);
|
||||
RUN_TEST(test_xattr);
|
||||
|
||||
printf("\n\033[1;36m=== TEST SUMMARY ===\033[0m\n");
|
||||
printf("Total Tests Run: %d\n", tests_run);
|
||||
|
||||
@@ -210,6 +210,60 @@ static void test_parse_args_version() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* --xattrs/-X and --acls/-A preserve per-file xattrs and both imply metadata
|
||||
* transmission (the xattr block rides the metadata/per-file frame); each is
|
||||
* individually negatable and the derived use_xattrs follows the flags. */
|
||||
static void test_parse_args_xattrs_acls() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "-X", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_TRUE(cfg->preserve_xattrs);
|
||||
EXPECT_FALSE(cfg->preserve_acls);
|
||||
EXPECT_TRUE(cfg->use_xattrs);
|
||||
EXPECT_TRUE(cfg->use_metadata);
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv_long[] = {"fastsync", "--acls", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv_long, positional_args, &positional_count), 0);
|
||||
EXPECT_TRUE(cfg->preserve_acls);
|
||||
EXPECT_TRUE(cfg->use_xattrs);
|
||||
EXPECT_TRUE(cfg->use_metadata);
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv_neg[] = {"fastsync", "-X", "-A", "--no-xattrs", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 6, argv_neg, positional_args, &positional_count), 0);
|
||||
EXPECT_FALSE(cfg->preserve_xattrs);
|
||||
EXPECT_TRUE(cfg->preserve_acls);
|
||||
EXPECT_TRUE(cfg->use_xattrs);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* --fake-super is a receiver-side preference that parks the source
|
||||
* uid/gid/mode/mtime in a reserved xattr; it implies metadata transmission. */
|
||||
static void test_parse_args_fake_super() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--fake-super", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_TRUE(cfg->fake_super);
|
||||
EXPECT_TRUE(cfg->use_metadata);
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv_neg[] = {"fastsync", "--fake-super", "--no-fake-super", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv_neg, positional_args, &positional_count), 0);
|
||||
EXPECT_FALSE(cfg->fake_super);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Test parse_args with valid port */
|
||||
static void test_parse_args_valid_port() {
|
||||
Config* cfg = config_create();
|
||||
@@ -772,6 +826,8 @@ static void test_parse_args_rejects_unimplemented_options() {
|
||||
"--acls",
|
||||
"-X",
|
||||
"--xattrs",
|
||||
"-D",
|
||||
"--devices",
|
||||
"--delete-excluded",
|
||||
"--max-delete",
|
||||
"--prune-empty-dirs",
|
||||
@@ -2526,6 +2582,8 @@ void test_client_cli() {
|
||||
test_parse_args_table_equals_size_options();
|
||||
test_parse_args_table_equals_string_and_int_options();
|
||||
test_parse_args_missing_argument_diagnostic();
|
||||
test_parse_args_xattrs_acls();
|
||||
test_parse_args_fake_super();
|
||||
test_parse_args_partial_progress();
|
||||
test_parse_args_itemize_changes();
|
||||
test_parse_args_list_only();
|
||||
|
||||
@@ -1183,6 +1183,49 @@ static void test_config_devices_wire_roundtrip() {
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase-4: preserve_xattrs/--acls (in file options) and --fake-super (trailing)
|
||||
* cross the config wire; the receiver recomputes the derived use_xattrs. */
|
||||
static void test_config_phase4_xattr_wire_roundtrip() {
|
||||
if (is_running_under_valgrind())
|
||||
return;
|
||||
Config* send_cfg = config_create();
|
||||
EXPECT_NOT_NULL(send_cfg);
|
||||
send_cfg->send_directory = str_dup("/send/src");
|
||||
send_cfg->receive_root_directory = str_dup("/send/dst");
|
||||
send_cfg->preserve_xattrs = true;
|
||||
send_cfg->preserve_acls = true;
|
||||
send_cfg->fake_super = true;
|
||||
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
close(p[1]);
|
||||
io_set_fds(p[0], p[0]);
|
||||
Config* recv = config_receive(p[0]);
|
||||
bool ok = recv != NULL;
|
||||
if (ok) {
|
||||
ok = recv->preserve_xattrs && recv->preserve_acls && recv->fake_super && recv->use_xattrs;
|
||||
}
|
||||
config_delete(recv);
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
_exit(ok ? 0 : 1);
|
||||
} else {
|
||||
close(p[0]);
|
||||
io_set_fds(p[1], p[1]);
|
||||
bool sent = config_send(p[1], send_cfg);
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
close(p[1]);
|
||||
config_delete(send_cfg);
|
||||
EXPECT_TRUE(sent);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void test_config() {
|
||||
test_config_lifecycle();
|
||||
test_config_ssh_dest();
|
||||
@@ -1213,6 +1256,7 @@ void test_config() {
|
||||
test_config_metadata_times_wire_roundtrip();
|
||||
test_config_devices_wire_roundtrip();
|
||||
test_config_preallocate_wire_roundtrip();
|
||||
test_config_phase4_xattr_wire_roundtrip();
|
||||
}
|
||||
test_config_delete_timing_early_helper();
|
||||
test_config_is_remote_dest();
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
#include "test_xattr.h"
|
||||
#include "xattr.h"
|
||||
#include "file.h"
|
||||
#include "protocol.h"
|
||||
#include "test_utils.h"
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/xattr.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void run_recv_helper(int fd) {
|
||||
int ok = 0;
|
||||
FileXattrList* list = xattr_receive(fd, &ok);
|
||||
if (!ok)
|
||||
_exit(1);
|
||||
if (!list) {
|
||||
/* NULL list only on error, already handled above. */
|
||||
_exit(1);
|
||||
}
|
||||
if (list->count != 2)
|
||||
_exit(1);
|
||||
if (strcmp(list->items[0].name, "user.foo") != 0 || list->items[0].value_len != 3 ||
|
||||
memcmp(list->items[0].value, "bar", 3) != 0)
|
||||
_exit(1);
|
||||
if (strcmp(list->items[1].name, "user.empty") != 0 || list->items[1].value_len != 0)
|
||||
_exit(1);
|
||||
xattr_list_free(list);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
static void test_xattr_wire_roundtrip() {
|
||||
/* Round-trip a user.* list incl. an empty value. */
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
close(p[1]);
|
||||
io_set_fds(p[0], p[0]);
|
||||
run_recv_helper(p[0]);
|
||||
}
|
||||
close(p[0]);
|
||||
io_set_fds(p[1], p[1]);
|
||||
FileXattrList* list = xattr_list_new();
|
||||
EXPECT_NOT_NULL(list);
|
||||
EXPECT_TRUE(xattr_list_append(list, "user.foo", "bar", 3));
|
||||
EXPECT_TRUE(xattr_list_append(list, "user.empty", NULL, 0));
|
||||
EXPECT_TRUE(xattr_send(p[1], list));
|
||||
xattr_list_free(list);
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
close(p[1]);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
|
||||
static void run_recv_must_fail(int fd) {
|
||||
int ok = 0;
|
||||
FileXattrList* list = xattr_receive(fd, &ok);
|
||||
/* A NULL list with ok==0 is the expected rejection. */
|
||||
if (ok == 0 && list == NULL)
|
||||
_exit(0);
|
||||
xattr_list_free(list);
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
/* A receiver must reject a security.* (privileged-namespace) attribute, never
|
||||
* apply it: the send side can be malicious, so only the receiver whitelist
|
||||
* matters. */
|
||||
static void test_xattr_reject_privileged_namespace() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
close(p[1]);
|
||||
io_set_fds(p[0], p[0]);
|
||||
run_recv_must_fail(p[0]);
|
||||
}
|
||||
close(p[0]);
|
||||
io_set_fds(p[1], p[1]);
|
||||
FileXattrList* list = xattr_list_new();
|
||||
EXPECT_NOT_NULL(list);
|
||||
/* security.capability must be rejected by the receiver. */
|
||||
EXPECT_TRUE(xattr_list_append(list, "security.capability", "\x01\x00", 2));
|
||||
xattr_send(p[1], list); /* receiver rejects at the name check and exits */
|
||||
xattr_list_free(list);
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
close(p[1]);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
|
||||
/* An oversized value (beyond XATTR_VALUE_MAX) must be rejected on receive. */
|
||||
static void test_xattr_reject_oversized_value() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
close(p[1]);
|
||||
io_set_fds(p[0], p[0]);
|
||||
run_recv_must_fail(p[0]);
|
||||
}
|
||||
close(p[0]);
|
||||
io_set_fds(p[1], p[1]);
|
||||
FileXattrList* list = xattr_list_new();
|
||||
EXPECT_NOT_NULL(list);
|
||||
size_t huge = (size_t)XATTR_VALUE_MAX + 1;
|
||||
unsigned char* blob = calloc(1, huge);
|
||||
EXPECT_NOT_NULL(blob);
|
||||
EXPECT_TRUE(xattr_list_append(list, "user.huge", blob, huge));
|
||||
xattr_send(p[1], list); /* send is best-effort; the receiver rejects and exits */
|
||||
free(blob);
|
||||
xattr_list_free(list);
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
close(p[1]);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
|
||||
/* The list append enforces the count bound (defense in depth). */
|
||||
static void test_xattr_count_bound() {
|
||||
FileXattrList* list = xattr_list_new();
|
||||
EXPECT_NOT_NULL(list);
|
||||
bool all_ok = true;
|
||||
for (int i = 0; i < XATTR_MAX_COUNT + 1; i++) {
|
||||
char name[32];
|
||||
snprintf(name, sizeof(name), "user.k%d", i);
|
||||
if (!xattr_list_append(list, name, "v", 1))
|
||||
all_ok = false;
|
||||
}
|
||||
EXPECT_FALSE(all_ok);
|
||||
EXPECT_EQ_INT(list->count, XATTR_MAX_COUNT);
|
||||
xattr_list_free(list);
|
||||
}
|
||||
|
||||
/* The captured list on a plain file reflects only whitelisted namespaces
|
||||
* (Linux only; skipped when the filesystem has no xattr support). */
|
||||
static void test_xattr_capture_and_appliable() {
|
||||
EXPECT_FALSE(xattr_name_appliable(NULL));
|
||||
EXPECT_FALSE(xattr_name_appliable(""));
|
||||
EXPECT_FALSE(xattr_name_appliable("security.selinux"));
|
||||
EXPECT_FALSE(xattr_name_appliable("trusted.blob"));
|
||||
EXPECT_TRUE(xattr_name_appliable("user.foo"));
|
||||
/* The reserved fake-super key is receiver-only and never forwarded/applied. */
|
||||
EXPECT_FALSE(xattr_name_appliable("user.fastsync.stat"));
|
||||
EXPECT_TRUE(xattr_name_appliable("system.posix_acl_access"));
|
||||
EXPECT_TRUE(xattr_name_appliable("system.posix_acl_default"));
|
||||
}
|
||||
|
||||
/* MINOR-2: a --link-dest / -H copy fallback (linkat refused) must still apply
|
||||
* the per-file xattrs and --fake-super stat. A DIRECTORY basis forces linkat
|
||||
* to fail with EPERM, exercising the byte-copy fallback deterministically.
|
||||
* Guarded on filesystem xattr support. */
|
||||
static void test_link_copy_fallback_preserves_xattrs() {
|
||||
const char* dest = "test_link_xattr_dest.txt";
|
||||
const char* basis_dir = "test_link_xattr_basis_dir";
|
||||
unlink(dest);
|
||||
rmdir(basis_dir);
|
||||
EXPECT_EQ_INT(mkdir(basis_dir, 0700), 0);
|
||||
|
||||
/* Probe xattr support on the cwd filesystem using the destination file. */
|
||||
int probe = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
bool has_xattr = probe >= 0 && setxattr(dest, "user.fastsync.xprobe", "p", 1, 0) == 0;
|
||||
if (probe >= 0)
|
||||
close(probe);
|
||||
if (!has_xattr) {
|
||||
removexattr(dest, "user.fastsync.xprobe");
|
||||
unlink(dest);
|
||||
rmdir(basis_dir);
|
||||
return; /* skip silently when the filesystem has no xattr support */
|
||||
}
|
||||
removexattr(dest, "user.fastsync.xprobe");
|
||||
|
||||
FileXattrList* xattrs = xattr_list_new();
|
||||
EXPECT_NOT_NULL(xattrs);
|
||||
EXPECT_TRUE(xattr_list_append(xattrs, "user.fallback", "kept", 4));
|
||||
|
||||
FileMetadata m;
|
||||
memset(&m, 0, sizeof(m));
|
||||
m.mode = 0640;
|
||||
m.uid = 1001;
|
||||
m.gid = 1002;
|
||||
m.mtime_sec = 1234567890;
|
||||
m.mtime_nsec = 0;
|
||||
m.atime_valid = false;
|
||||
m.crtime_valid = false;
|
||||
|
||||
bool ok = file_to_disk_secure_link_attrs(dest, basis_dir, "payload", 7, false, &m, false, false,
|
||||
xattrs, true, NULL);
|
||||
xattr_list_free(xattrs);
|
||||
EXPECT_TRUE(ok);
|
||||
|
||||
/* Content landed (the copy fallback wrote the caller's bytes). */
|
||||
int fd = open(dest, O_RDONLY);
|
||||
EXPECT_TRUE(fd >= 0);
|
||||
if (fd >= 0) {
|
||||
char buf[16];
|
||||
ssize_t n = read(fd, buf, sizeof(buf));
|
||||
close(fd);
|
||||
EXPECT_EQ_INT((int)strlen("payload"), (int)n);
|
||||
if (n == 7)
|
||||
EXPECT_TRUE(memcmp(buf, "payload", 7) == 0);
|
||||
}
|
||||
/* Per-file xattr applied on the copy. */
|
||||
char vbuf[16];
|
||||
ssize_t vlen = getxattr(dest, "user.fallback", vbuf, sizeof(vbuf));
|
||||
EXPECT_EQ_INT(4, (int)vlen);
|
||||
if (vlen == 4)
|
||||
EXPECT_TRUE(memcmp(vbuf, "kept", 4) == 0);
|
||||
/* fake-super stat parked by the receiver. */
|
||||
EXPECT_TRUE((int)getxattr(dest, FAKESUPER_XATTR, NULL, 0) > 0);
|
||||
|
||||
unlink(dest);
|
||||
rmdir(basis_dir);
|
||||
}
|
||||
|
||||
void test_xattr() {
|
||||
test_xattr_wire_roundtrip();
|
||||
test_xattr_reject_privileged_namespace();
|
||||
test_xattr_reject_oversized_value();
|
||||
test_xattr_count_bound();
|
||||
test_xattr_capture_and_appliable();
|
||||
test_link_copy_fallback_preserves_xattrs();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_XATTR_H
|
||||
#define TEST_XATTR_H
|
||||
|
||||
void test_xattr(void);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user