Re: [RFC PATCH v4 6/8] selftests/fuse: add some extra ACL caching tests
From: Amir Goldstein
Date: Thu Sep 17 2026 - 11:26:49 EST
On Wed, Sep 16, 2026 at 5:55 PM Luis Henriques <luis@xxxxxxxxxx> wrote:
>
> This adds some extra tests to ACL caching:
> - Verify that reading ACLs results in the expected number of requests
> being sent user-space, depending on whether cache is enabled or disabled
> - Verify caching behaviour on some caching invalidation scenarios
>
> While there, add test binary to .gitignore.
>
> Signed-off-by: Luis Henriques <luis@xxxxxxxxxx>
> ---
> .../selftests/filesystems/fuse/.gitignore | 1 +
> .../filesystems/fuse/fuse_acl_cache_test.c | 203 +++++++++++++++++-
> 2 files changed, 202 insertions(+), 2 deletions(-)
>
> diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore
> index fb51603fe419..f7f3dd345a50 100644
> --- a/tools/testing/selftests/filesystems/fuse/.gitignore
> +++ b/tools/testing/selftests/filesystems/fuse/.gitignore
> @@ -2,3 +2,4 @@
> fuse_mnt
> fusectl_test
> write_extend_eof_test
> +fuse_acl_cache_test
> diff --git a/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c b/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c
> index fb4e3554e3c9..173063efeee8 100644
> --- a/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c
> +++ b/tools/testing/selftests/filesystems/fuse/fuse_acl_cache_test.c
> @@ -84,6 +84,9 @@ struct daemon_state {
> uint8_t *acl;
> size_t acl_size;
> int getxattr_count;
> + uid_t uid;
> + uid_t gid;
> + bool cache;
> };
>
> /*
> @@ -92,9 +95,17 @@ struct daemon_state {
> */
> static struct daemon_state g_ds = {
> .lock = PTHREAD_MUTEX_INITIALIZER,
> + .cache = false,
> };
>
> /* ---- FUSE lowlevel callbacks -------------------------------------------- */
> +static void fs_init(void *userdata, struct fuse_conn_info *conn)
> +{
> + pthread_mutex_lock(&g_ds.lock);
> + if (g_ds.cache)
> + fuse_set_feature_flag(conn, FUSE_CAP_POSIX_ACL);
> + pthread_mutex_unlock(&g_ds.lock);
> +}
>
> static void fs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name)
> {
> @@ -116,6 +127,10 @@ static void fs_lookup(fuse_req_t req, fuse_ino_t parent, const char *name)
> e.attr.st_ino = FILE_INO;
> e.attr.st_mode = S_IFREG | 0644;
> e.attr.st_nlink = 1;
> + pthread_mutex_lock(&g_ds.lock);
> + e.attr.st_uid = g_ds.uid;
> + e.attr.st_gid = g_ds.gid;
> + pthread_mutex_unlock(&g_ds.lock);
> fuse_reply_entry(req, &e);
> }
>
> @@ -133,6 +148,10 @@ static void fs_getattr(fuse_req_t req, fuse_ino_t ino,
> st.st_ino = FILE_INO;
> st.st_mode = S_IFREG | 0644;
> st.st_nlink = 1;
> + pthread_mutex_lock(&g_ds.lock);
> + st.st_uid = g_ds.uid;
> + st.st_gid = g_ds.gid;
> + pthread_mutex_unlock(&g_ds.lock);
> } else {
> fuse_reply_err(req, ENOENT);
> return;
> @@ -161,12 +180,14 @@ static void fs_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name,
> }
> pthread_mutex_lock(&g_ds.lock);
> acl_size = g_ds.acl_size;
> - if (acl && (size >= acl_size))
> + if (acl && acl_size && (size >= acl_size))
> memcpy(acl, g_ds.acl, acl_size);
> g_ds.getxattr_count++;
> pthread_mutex_unlock(&g_ds.lock);
>
> - if (size == 0)
> + if (acl_size == 0)
> + fuse_reply_err(req, ENODATA);
> + else if (size == 0)
> fuse_reply_xattr(req, acl_size);
> else if (size < acl_size)
> fuse_reply_err(req, ERANGE);
> @@ -176,10 +197,42 @@ static void fs_getxattr(fuse_req_t req, fuse_ino_t ino, const char *name,
> free(acl);
> }
>
> +static void fs_setxattr(fuse_req_t req, fuse_ino_t ino, const char *name,
> + const char *value, size_t size, int flags)
> +{
> + int ret = 0;
> + uint8_t *acl;
> +
> + if (ino != FILE_INO) {
> + ret = ENOENT;
> + goto out;
> + } else if (strcmp(name, "system.posix_acl_access") != 0) {
> + ret = ENOTSUP;
> + goto out;
> + }
> + acl = malloc(size);
> + if (!acl) {
> + ret = ENOMEM;
> + goto out;
> + }
> + memcpy(acl, value, size);
> + pthread_mutex_lock(&g_ds.lock);
> + if (g_ds.acl)
> + free(g_ds.acl);
> + g_ds.acl = acl;
> + g_ds.acl_size = size;
> + pthread_mutex_unlock(&g_ds.lock);
> +
> +out:
> + fuse_reply_err(req, ret);
> +}
> +
> static const struct fuse_lowlevel_ops fs_ops = {
> + .init = fs_init,
> .lookup = fs_lookup,
> .getattr = fs_getattr,
> .getxattr = fs_getxattr,
> + .setxattr = fs_setxattr,
> };
>
> /* ---- kselftest harness --------------------------------------------------- */
> @@ -198,6 +251,9 @@ FIXTURE_SETUP(acl_cache)
> ASSERT_NE(g_ds.acl, NULL);
> memcpy(g_ds.acl, acl_a, g_ds.acl_size);
> g_ds.getxattr_count = 0;
> + g_ds.uid = getuid();
> + g_ds.gid = getgid();
> + g_ds.cache = false;
>
> if (fs_setup(&fs_ops, &g_ds.ctx, err))
> SKIP(goto out, err);
> @@ -325,4 +381,147 @@ TEST_F(acl_cache, stale_after_force_sync)
> EXPECT_EQ(count, 4);
> }
>
> +FIXTURE(acl_cache_onoff)
> +{
> + char pathname[PATH_MAX];
> +};
> +
> +FIXTURE_VARIANT(acl_cache_onoff) { bool cache; };
> +FIXTURE_VARIANT_ADD(acl_cache_onoff, nocache) { .cache = false, };
> +FIXTURE_VARIANT_ADD(acl_cache_onoff, docache) { .cache = true, };
> +
> +FIXTURE_SETUP(acl_cache_onoff)
> +{
> + char err[MAX_ERR_MSG];
> +
> + pthread_mutex_lock(&g_ds.lock);
> + g_ds.acl = NULL;
> + g_ds.acl_size = 0;
> + g_ds.getxattr_count = 0;
> + g_ds.cache = variant->cache;
> + g_ds.uid = getuid();
> + g_ds.gid = getgid();
> +
> + if (fs_setup(&fs_ops, &g_ds.ctx, err))
> + SKIP(goto out, err);
> +
> + snprintf(self->pathname, sizeof(self->pathname),
> + "%s/" FILE_NAME, g_ds.ctx.mountpoint);
> +out:
> + pthread_mutex_unlock(&g_ds.lock);
> +}
> +
> +FIXTURE_TEARDOWN(acl_cache_onoff)
> +{
> + pthread_mutex_lock(&g_ds.lock);
> + fs_teardown(&g_ds.ctx);
> + pthread_mutex_unlock(&g_ds.lock);
> + free(g_ds.acl);
> +}
> +
> +/*
> + * This is the most basic ACL caching test: verify that, when reading ACLs for
> + * an inode, user-space is called:
> + * - Only once if ACLs cache is enabled, or
> + * - Once per access if cache i disabled.
Typo "is disabled"
> + */
> +TEST_F(acl_cache_onoff, test_acl_cache_enable_disable)
> +{
> + char buf[512];
> + ssize_t sz;
> + bool cache;
> + int counter;
> + int i;
> +
> + ASSERT_EQ(lsetxattr(self->pathname, "system.posix_acl_access",
> + acl_a, sizeof(acl_a), 0), 0);
> +
> + for (i = 0; i < 100; i++) {
> + sz = lgetxattr(self->pathname, "system.posix_acl_access",
> + buf, sizeof(buf));
> + ASSERT_EQ(sz, sizeof(acl_a));
> + ASSERT_EQ(memcmp(buf, acl_a, sz), 0);
> + }
> +
> + pthread_mutex_lock(&g_ds.lock);
> + counter = g_ds.getxattr_count;
> + cache = g_ds.cache;
> + pthread_mutex_unlock(&g_ds.lock);
> +
> + if (cache) {
> + ASSERT_EQ(counter, 1);
> + } else {
> + ASSERT_EQ(counter, 100);
> + }
> +
> + TH_LOG("User-space called %d time(s) with ACL caching %s",
> + counter, cache ? "enabled" : "disabled");
> +}
> +
> +/*
> + * Test caching invalidation for several scenarios:
> + * 1. When a new ACL is set
> + * 2. When invalidating an inode (NOTIFY_INODE_INVAL)
Typo NOTIFY_INVAL_INODE
With that fix, feel free to add
Reviewed-by: Amir Goldstein <amir73il@xxxxxxxxx>
Thanks,
Amir.
> + */
> +TEST_F(acl_cache_onoff, test_acl_cache_invalidation)
> +{
> + char buf[512];
> + ssize_t sz;
> + int counter;
> + bool cache;
> + int i;
> +
> + /* Set an ACL */
> + ASSERT_EQ(lsetxattr(self->pathname, "system.posix_acl_access",
> + acl_a, sizeof(acl_a), 0), 0);
> +
> + for (i = 0; i < 100; i++) {
> + sz = lgetxattr(self->pathname, "system.posix_acl_access",
> + buf, sizeof(buf));
> + ASSERT_EQ(sz, sizeof(acl_a));
> + ASSERT_EQ(memcmp(buf, acl_a, sz), 0);
> + }
> +
> + /* 1. force cache invalidation by setting a new ACL */
> + ASSERT_EQ(lsetxattr(self->pathname, "system.posix_acl_access",
> + acl_b, sizeof(acl_b), 0), 0);
> +
> + sz = lgetxattr(self->pathname, "system.posix_acl_access",
> + buf, sizeof(buf));
> + ASSERT_EQ(sz, sizeof(acl_b));
> + ASSERT_EQ(memcmp(buf, acl_b, sz), 0);
> +
> + pthread_mutex_lock(&g_ds.lock);
> + counter = g_ds.getxattr_count;
> + cache = g_ds.cache;
> + pthread_mutex_unlock(&g_ds.lock);
> +
> + if (cache) {
> + ASSERT_EQ(counter, 2);
> + } else {
> + ASSERT_EQ(counter, 101);
> + }
> + TH_LOG("Invalidation by setting new ACL: OK");
> +
> + /* 2. send FUSE_NOTIFY_INVAL_INODE */
> + fuse_lowlevel_notify_inval_inode(g_ds.ctx.se, FILE_INO, 0, 0);
> +
> + sz = lgetxattr(self->pathname, "system.posix_acl_access",
> + buf, sizeof(buf));
> + ASSERT_EQ(sz, sizeof(acl_b));
> + ASSERT_EQ(memcmp(buf, acl_b, sz), 0);
> +
> + pthread_mutex_lock(&g_ds.lock);
> + counter = g_ds.getxattr_count;
> + cache = g_ds.cache;
> + pthread_mutex_unlock(&g_ds.lock);
> +
> + if (cache) {
> + ASSERT_EQ(counter, 3);
> + } else {
> + ASSERT_EQ(counter, 102);
> + }
> + TH_LOG("Invalidation through FUSE_NOTIFY_INVAL_INODE: OK");
> +}
> +
> TEST_HARNESS_MAIN