feat(xattr): carry and apply symlink xattrs (protocol 2.29.0)
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
#include "test_xattr.h"
|
||||
#include "xattr.h"
|
||||
#include "charset.h"
|
||||
#include "config.h"
|
||||
#include "file.h"
|
||||
#include "file_receive.h"
|
||||
#include "file_save.h"
|
||||
#include "identity.h"
|
||||
#include "metadata.h"
|
||||
#include "protocol.h"
|
||||
#include "test_utils.h"
|
||||
#include <fcntl.h>
|
||||
@@ -11,6 +14,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/xattr.h>
|
||||
@@ -660,8 +664,187 @@ static void test_file_save_directory_applies_xattrs() {
|
||||
rmdir(root);
|
||||
}
|
||||
|
||||
/* Symlink xattrs (protocol 2.29.0): the no-follow capture must read the LINK's
|
||||
* OWN attributes and never the REFERENT's. Linux's VFS refuses to associate
|
||||
* xattrs with a symlink at all, so the nofollow capture returns NULL while the
|
||||
* path-following capture sees the referent's attribute -- which is exactly the
|
||||
* bug the no-follow variant exists to prevent (a symlink entry must not carry
|
||||
* its target's attributes). Guarded on filesystem xattr support. */
|
||||
static void test_xattr_capture_symlink_nofollow() {
|
||||
const char* target = "test_symlink_xattr_capture_target";
|
||||
const char* link = "test_symlink_xattr_capture_link";
|
||||
unlink(link);
|
||||
unlink(target);
|
||||
int fd = open(target, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (fd < 0)
|
||||
return;
|
||||
bool has_xattr = setxattr(target, "user.symref", "referent", 8, 0) == 0;
|
||||
close(fd);
|
||||
if (!has_xattr) {
|
||||
unlink(target);
|
||||
return; /* filesystem without xattr support */
|
||||
}
|
||||
if (symlink(target, link) != 0) {
|
||||
unlink(target);
|
||||
return;
|
||||
}
|
||||
|
||||
/* The no-follow capture must never pick up the referent's attributes. */
|
||||
FileXattrList* nofollow = xattr_capture_path_nofollow(link, false);
|
||||
EXPECT_NULL(nofollow);
|
||||
|
||||
/* The path-following capture does, proving the referent really carries one
|
||||
and that the no-follow variant differs. */
|
||||
FileXattrList* follow = xattr_capture_path(link, false);
|
||||
bool saw = false;
|
||||
for (int i = 0; follow && i < follow->count; i++) {
|
||||
if (strcmp(follow->items[i].name, "user.symref") == 0)
|
||||
saw = true;
|
||||
}
|
||||
EXPECT_TRUE(saw);
|
||||
xattr_list_free(follow);
|
||||
xattr_list_free(nofollow);
|
||||
unlink(link);
|
||||
unlink(target);
|
||||
}
|
||||
|
||||
/* Symlink xattrs (protocol 2.29.0): the no-follow apply must target the LINK,
|
||||
* never its referent. On Linux the LSETXATTR is refused (the VFS does not
|
||||
* allow symlink xattrs), but the critical guarantee is observable: the
|
||||
* referent's attributes are UNCHANGED. A regression from lsetxattr to the
|
||||
* path-following setxattr would rewrite the referent here and fail this test. */
|
||||
static void test_xattr_apply_path_nofollow_does_not_follow() {
|
||||
const char* root = "test_symlink_xattr_apply_tmp";
|
||||
const char* target = "test_symlink_xattr_apply_tmp/target";
|
||||
const char* link = "test_symlink_xattr_apply_tmp/link";
|
||||
unlink(link);
|
||||
unlink(target);
|
||||
rmdir(root);
|
||||
EXPECT_EQ_INT(mkdir(root, 0700), 0);
|
||||
int tfd = open(target, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (tfd < 0) {
|
||||
rmdir(root);
|
||||
return;
|
||||
}
|
||||
bool has_xattr = setxattr(target, "user.orig", "orig", 4, 0) == 0;
|
||||
close(tfd);
|
||||
if (!has_xattr) {
|
||||
unlink(target);
|
||||
rmdir(root);
|
||||
return; /* filesystem without xattr support */
|
||||
}
|
||||
EXPECT_EQ_INT(symlink("target", link), 0);
|
||||
|
||||
FileXattrList* list = xattr_list_new();
|
||||
EXPECT_NOT_NULL(list);
|
||||
EXPECT_TRUE(xattr_list_append(list, "user.orig", "hacked", 6));
|
||||
EXPECT_TRUE(xattr_list_append(list, "user.added", "x", 1));
|
||||
|
||||
/* Invalid anchors are refused before any syscall (no fd/leaf/list). */
|
||||
EXPECT_FALSE(xattr_apply_path_nofollow(-1, "link", list));
|
||||
EXPECT_FALSE(xattr_apply_path_nofollow(0, "", list));
|
||||
EXPECT_FALSE(xattr_apply_path_nofollow(0, "a/b", list));
|
||||
EXPECT_FALSE(xattr_apply_path_nofollow(0, "link", NULL));
|
||||
|
||||
/* The confined parent directory is the anchor; the final component is the
|
||||
link. Best-effort: returns true even when the kernel refuses. */
|
||||
int dir_fd = open(root, O_RDONLY | O_DIRECTORY);
|
||||
EXPECT_TRUE(dir_fd >= 0);
|
||||
EXPECT_TRUE(xattr_apply_path_nofollow(dir_fd, "link", list));
|
||||
close(dir_fd);
|
||||
|
||||
/* The referent must be untouched: a following apply would have set user.orig
|
||||
to "hacked" and created user.added on the target. */
|
||||
char buf[16];
|
||||
ssize_t got = getxattr(target, "user.orig", buf, sizeof(buf));
|
||||
EXPECT_EQ_INT(4, (int)got);
|
||||
if (got == 4)
|
||||
EXPECT_TRUE(memcmp(buf, "orig", 4) == 0);
|
||||
EXPECT_TRUE(getxattr(target, "user.added", buf, sizeof(buf)) < 0);
|
||||
|
||||
/* If the platform DOES support symlink xattrs, they must have landed on the
|
||||
link itself; on Linux the VFS refuses them, so the link stays empty. */
|
||||
if (llistxattr(link, NULL, 0) > 0) {
|
||||
ssize_t n = lgetxattr(link, "user.added", buf, sizeof(buf));
|
||||
EXPECT_EQ_INT(1, (int)n);
|
||||
if (n == 1)
|
||||
EXPECT_TRUE(buf[0] == 'x');
|
||||
}
|
||||
|
||||
xattr_list_free(list);
|
||||
unlink(link);
|
||||
unlink(target);
|
||||
rmdir(root);
|
||||
}
|
||||
|
||||
/* Protocol 2.29.0: a STATUS_SYMLINK frame followed by an -X/-A xattr block is
|
||||
* decoded by file_receive_symlink() with the block attached to the File. This
|
||||
* is the wire round-trip for the new trailing symlink xattr block. */
|
||||
static void run_recv_symlink_with_xattrs(int fd) {
|
||||
/* Stack-allocated so the forked child leaks nothing at _exit() (a
|
||||
config_create() in the parent would be inherited and never freed here). */
|
||||
Config config;
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.use_metadata = true;
|
||||
config.use_xattrs = true;
|
||||
config.preserve_xattrs = true;
|
||||
File* file = file_receive_symlink(fd, &config);
|
||||
if (!file)
|
||||
_exit(1);
|
||||
bool ok = file->is_symlink && file->symlink_target != NULL &&
|
||||
strcmp(file->symlink_target, "target") == 0;
|
||||
ok = ok && file->xattrs != NULL && file->xattrs->count == 1 &&
|
||||
strcmp(file->xattrs->items[0].name, "user.sym") == 0 &&
|
||||
file->xattrs->items[0].value_len == 3 && memcmp(file->xattrs->items[0].value, "sym", 3) == 0;
|
||||
file_destroy(file);
|
||||
_exit(ok ? 0 : 1);
|
||||
}
|
||||
|
||||
static void test_symlink_frame_carries_xattrs() {
|
||||
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]);
|
||||
io_set_bwlimit(0);
|
||||
run_recv_symlink_with_xattrs(p[0]);
|
||||
}
|
||||
close(p[0]);
|
||||
io_set_fds(p[1], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
FileMetadata m;
|
||||
memset(&m, 0, sizeof(m));
|
||||
m.mode = S_IFLNK | 0777;
|
||||
m.uid = (uint32_t)geteuid();
|
||||
m.gid = (uint32_t)getegid();
|
||||
m.mtime_sec = 1700000000;
|
||||
|
||||
FileXattrList* list = xattr_list_new();
|
||||
EXPECT_NOT_NULL(list);
|
||||
EXPECT_TRUE(xattr_list_append(list, "user.sym", "sym", 3));
|
||||
|
||||
/* Exactly the sender's order: path, target, metadata, xattr block. */
|
||||
bool wrote = send_wire_str(p[1], "link") && send_wire_str(p[1], "target") &&
|
||||
metadata_send(p[1], &m) && xattr_send(p[1], list);
|
||||
xattr_list_free(list);
|
||||
close(p[1]);
|
||||
|
||||
int status = 0;
|
||||
waitpid(pid, &status, 0);
|
||||
EXPECT_TRUE(wrote);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
|
||||
void test_xattr() {
|
||||
test_xattr_list_clone();
|
||||
test_xattr_capture_symlink_nofollow();
|
||||
test_xattr_apply_path_nofollow_does_not_follow();
|
||||
test_symlink_frame_carries_xattrs();
|
||||
test_xattr_wire_roundtrip();
|
||||
test_xattr_reject_privileged_namespace();
|
||||
test_xattr_reject_oversized_value();
|
||||
|
||||
Reference in New Issue
Block a user