#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/xattr.h>
#include <unistd.h>
#define FUSE_USE_VERSION 31
#include <fuse_lowlevel.h>
#include "kselftest_harness.h"
static const uint8_t acl_a[] = {
0x02, 0x00, 0x00, 0x00,
0x01, 0x00, 0x07, 0x00, 0xff, 0xff, 0xff, 0xff,
0x04, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff,
0x20, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff,
};
static const uint8_t acl_b[] = {
0x02, 0x00, 0x00, 0x00,
0x01, 0x00, 0x07, 0x00, 0xff, 0xff, 0xff, 0xff,
0x02, 0x00, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00,
0x04, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff,
0x10, 0x00, 0x07, 0x00, 0xff, 0xff, 0xff, 0xff,
0x20, 0x00, 0x05, 0x00, 0xff, 0xff, 0xff, 0xff,
};
#define FILE_INO 2
#define FILE_NAME "testfile"
struct daemon_state {
pthread_mutex_t lock;
const uint8_t *acl;
size_t acl_size;
int getxattr_count;
};
static struct daemon_state g_ds = {
.lock = PTHREAD_MUTEX_INITIALIZER,
};
static void fs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name)
{
if (parent != FUSE_ROOT_ID || strcmp(name, FILE_NAME)) {
fuse_reply_err(req, ENOENT);
return;
}
struct fuse_entry_param e = {};
e.ino = FILE_INO;
e.generation = 1;
e.attr_timeout = 10.0;
e.entry_timeout = 10.0;
e.attr.st_ino = FILE_INO;
e.attr.st_mode = S_IFREG | 0644;
e.attr.st_nlink = 1;
fuse_reply_entry(req, &e);
}
static void fs_getattr(fuse_req_t req, fuse_ino_t ino,
struct fuse_file_info *fi)
{
struct stat st = {};
(void)fi;
if (ino == FUSE_ROOT_ID) {
st.st_ino = FUSE_ROOT_ID;
st.st_mode = S_IFDIR | 0755;
st.st_nlink = 2;
} else if (ino == FILE_INO) {
st.st_ino = FILE_INO;
st.st_mode = S_IFREG | 0644;
st.st_nlink = 1;
} else {
fuse_reply_err(req, ENOENT);
return;
}
fuse_reply_attr(req, &st, 10);
}
static void fs_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name,
size_t size)
{
if (ino != FILE_INO ||
strcmp(name, "system.posix_acl_access") != 0) {
fuse_reply_err(req, ENODATA);
return;
}
pthread_mutex_lock(&g_ds.lock);
const uint8_t *acl = g_ds.acl;
size_t acl_size = g_ds.acl_size;
g_ds.getxattr_count++;
pthread_mutex_unlock(&g_ds.lock);
if (size == 0)
fuse_reply_xattr(req, acl_size);
else if (size < acl_size)
fuse_reply_err(req, ERANGE);
else
fuse_reply_buf(req, (const char *)acl, acl_size);
}
static const struct fuse_lowlevel_ops fs_ops = {
.lookup = fs_lookup,
.getattr = fs_getattr,
.getxattr = fs_getxattr,
};
static void *run_daemon(void *arg)
{
fuse_session_loop((struct fuse_session *)arg);
return NULL;
}
FIXTURE(acl_cache) {
struct fuse_session *se;
char mountpoint[PATH_MAX];
char file_path[PATH_MAX];
pthread_t thread;
};
FIXTURE_SETUP(acl_cache)
{
char *fuse_argv[] = { "fuse_acl_cache_test", NULL };
struct fuse_args args = FUSE_ARGS_INIT(1, fuse_argv);
g_ds.acl = acl_a;
g_ds.acl_size = sizeof(acl_a);
g_ds.getxattr_count = 0;
strcpy(self->mountpoint, "/tmp/acl_cache_test_XXXXXX");
if (!mkdtemp(self->mountpoint))
SKIP(return, "mkdtemp: %s", strerror(errno));
snprintf(self->file_path, sizeof(self->file_path),
"%s/" FILE_NAME, self->mountpoint);
self->se = fuse_session_new(&args, &fs_ops, sizeof(fs_ops), NULL);
if (!self->se) {
rmdir(self->mountpoint);
SKIP(return, "fuse_session_new failed");
}
if (fuse_session_mount(self->se, self->mountpoint)) {
fuse_session_destroy(self->se);
rmdir(self->mountpoint);
SKIP(return, "fuse_session_mount failed "
"(missing fusermount3 or insufficient privileges)");
}
if (pthread_create(&self->thread, NULL, run_daemon, self->se)) {
fuse_session_unmount(self->se);
fuse_session_destroy(self->se);
rmdir(self->mountpoint);
SKIP(return, "pthread_create: %s", strerror(errno));
}
fuse_opt_free_args(&args);
}
FIXTURE_TEARDOWN(acl_cache)
{
fuse_session_exit(self->se);
fuse_session_unmount(self->se);
pthread_join(self->thread, NULL);
fuse_session_destroy(self->se);
rmdir(self->mountpoint);
}
static int do_force_statx(const char *path)
{
struct statx stx;
return statx(AT_FDCWD, path, AT_STATX_FORCE_SYNC, STATX_BASIC_STATS,
&stx);
}
TEST_F(acl_cache, stale_after_force_sync)
{
char buf[512];
ssize_t sz;
int count;
sz = lgetxattr(self->file_path, "system.posix_acl_access",
buf, sizeof(buf));
ASSERT_EQ(sz, (ssize_t)sizeof(acl_a));
sz = lgetxattr(self->file_path, "system.posix_acl_access",
buf, sizeof(buf));
ASSERT_EQ(sz, (ssize_t)sizeof(acl_a));
pthread_mutex_lock(&g_ds.lock);
count = g_ds.getxattr_count;
pthread_mutex_unlock(&g_ds.lock);
ASSERT_EQ(count, 2);
TH_LOG("step 1 OK: both pre-trigger getxattrs reached daemon (count=%d), "
"ACL_DONT_CACHE is working", count);
ASSERT_EQ(do_force_statx(self->file_path), 0);
TH_LOG("step 2 OK: statx(AT_STATX_FORCE_SYNC) succeeded");
sz = lgetxattr(self->file_path, "system.posix_acl_access",
buf, sizeof(buf));
ASSERT_EQ(sz, (ssize_t)sizeof(acl_a));
pthread_mutex_lock(&g_ds.lock);
count = g_ds.getxattr_count;
pthread_mutex_unlock(&g_ds.lock);
ASSERT_EQ(count, 3);
TH_LOG("step 3 OK: post-trigger getxattr reached daemon (count=%d), "
"returned correct ACL_A (%zd bytes)", count, sz);
pthread_mutex_lock(&g_ds.lock);
g_ds.acl = acl_b;
g_ds.acl_size = sizeof(acl_b);
pthread_mutex_unlock(&g_ds.lock);
TH_LOG("step 4: daemon switched to ACL_B (%zu bytes)", sizeof(acl_b));
sz = lgetxattr(self->file_path, "system.posix_acl_access",
buf, sizeof(buf));
pthread_mutex_lock(&g_ds.lock);
count = g_ds.getxattr_count;
pthread_mutex_unlock(&g_ds.lock);
if (sz == (ssize_t)sizeof(acl_a))
TH_LOG("step 5 BUG: stale ACL_A (%zd bytes) from kernel cache "
"(count=%d); ACL_DONT_CACHE corrupted by "
"forget_all_cached_acls()", sz, count);
else
TH_LOG("step 5 OK: daemon reached (count=%d), "
"fresh ACL_B (%zd bytes)", count, sz);
EXPECT_EQ(sz, (ssize_t)sizeof(acl_b));
EXPECT_EQ(count, 4);
}
TEST_HARNESS_MAIN