about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--support/Makefile2
-rw-r--r--support/fuse.h215
-rw-r--r--support/support_fuse.c705
-rw-r--r--support/tst-support_fuse.c348
4 files changed, 1270 insertions, 0 deletions
diff --git a/support/Makefile b/support/Makefile
index 8fb4d2c500..93d32ae75f 100644
--- a/support/Makefile
+++ b/support/Makefile
@@ -64,6 +64,7 @@ libsupport-routines = \
   support_format_herrno \
   support_format_hostent \
   support_format_netent \
+  support_fuse \
   support_isolate_in_subprocess \
   support_mutex_pi_monotonic \
   support_need_proc \
@@ -327,6 +328,7 @@ tests = \
   tst-support_capture_subprocess \
   tst-support_descriptors \
   tst-support_format_dns_packet \
+  tst-support_fuse \
   tst-support_quote_blob \
   tst-support_quote_blob_wide \
   tst-support_quote_string \
diff --git a/support/fuse.h b/support/fuse.h
new file mode 100644
index 0000000000..4c365fbc0c
--- /dev/null
+++ b/support/fuse.h
@@ -0,0 +1,215 @@
+/* Facilities for FUSE-backed file system tests.
+   Copyright (C) 2024 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+/* Before using this functionality, use support_enter_mount_namespace
+   to ensure that mounts do not impact the overall system.  */
+
+#ifndef SUPPORT_FUSE_H
+#define SUPPORT_FUSE_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include <support/bundled/linux/include/uapi/linux/fuse.h>
+
+/* This function must be called furst, before support_fuse_mount, to
+   prepare unprivileged mounting.  */
+void support_fuse_init (void);
+
+/* This function can be called instead of support_fuse_init.  It does
+   not use mount and user namespaces, so it requires root privileges,
+   and cleanup after testing may be incomplete.  This is intended only
+   for test development.  */
+void support_fuse_init_no_namespace (void);
+
+/* Opaque type for tracking FUSE mount state.  */
+struct support_fuse;
+
+/* This function disables a mount point created using
+   support_fuse_mount.  */
+void support_fuse_unmount (struct support_fuse *) __nonnull ((1));
+
+/* This function is called on a separate thread after calling
+   support_fuse_mount.  F is the mount state, and CLOSURE the argument
+   that was passed to support_fuse_mount.  The callback function is
+   expected to call support_fuse_next to read packets from the kernel
+   and handle them according to the test's need.  */
+typedef void (*support_fuse_callback) (struct support_fuse *f, void *closure);
+
+/* This function creates a new mount point, implemented by CALLBACK.
+   CLOSURE is passed to CALLBACK as the second argument.  */
+struct support_fuse *support_fuse_mount (support_fuse_callback callback,
+                                         void *closure)
+  __nonnull ((1)) __attr_dealloc (support_fuse_unmount, 1);
+
+/* This function returns the path to the mount point for F.  The
+   returned string is valid until support_fuse_unmount (F) is called.  */
+const char * support_fuse_mountpoint (struct support_fuse *f) __nonnull ((1));
+
+
+/* Renders the OPCODE as a string (FUSE_* constant.  The caller must
+   free the returned string.  */
+char * support_fuse_opcode (uint32_t opcode) __attr_dealloc_free;
+
+/* Use to provide a checked cast facility.  Use the
+   support_fuse_in_cast macro below.  */
+void *support_fuse_cast_internal (struct fuse_in_header *, uint32_t)
+  __nonnull ((1));
+void *support_fuse_cast_name_internal (struct fuse_in_header *, uint32_t,
+                                       size_t skip, char **name)
+  __nonnull ((1));
+
+/* The macro expansion support_fuse_in_cast (P, TYPE) casts the
+   pointer INH to the appropriate type corresponding to the FUSE_TYPE
+   opcode.  It fails (terminates the process) if INH->opcode does not
+   match FUSE_TYPE.  The type of the returned pointer matches that of
+   the FUSE_* constant.
+
+   Maintenance note: Adding support for additional struct fuse_*_in
+   types is generally easy, except when there is trailing data after
+   the struct (see below for support_fuse_cast_name, for example), and
+   the kernel has changed struct sizes over time.  This has happened
+   recently with struct fuse_setxattr_in, and would require special
+   handling if implemented.  */
+#define support_fuse_payload_type_INIT struct fuse_init_in
+#define support_fuse_payload_type_LOOKUP char
+#define support_fuse_payload_type_OPEN struct fuse_open_in
+#define support_fuse_payload_type_READ struct fuse_read_in
+#define support_fuse_payload_type_SETATTR struct fuse_setattr_in
+#define support_fuse_payload_type_WRITE struct fuse_write_in
+#define support_fuse_cast(typ, inh)                     \
+  ((support_fuse_payload_type_##typ *)                  \
+   support_fuse_cast_internal ((inh), FUSE_##typ))
+
+/* Same as support_fuse_cast, but also writes the passed name to *NAMEP.  */
+#define support_fuse_payload_name_type_CREATE struct fuse_create_in
+#define support_fuse_payload_name_type_MKDIR struct fuse_mkdir_in
+#define support_fuse_cast_name(typ, inh, namep)                         \
+  ((support_fuse_payload_name_type_##typ *)                             \
+   support_fuse_cast_name_internal                                      \
+   ((inh), FUSE_##typ, sizeof (support_fuse_payload_name_type_##typ),   \
+    (namep)))
+
+/* This function should be called from the callback function.  It
+   returns NULL if the mount point has been unmounted.  The result can
+   be cast using support_fuse_in_cast.  The pointer is invalidated
+   with the next call to support_fuse_next.
+
+   Typical use involves handling some basics using the
+   support_fuse_handle_* building blocks, following by a switch
+   statement on the result member of the returned struct, to implement
+   what a particular test needs.  Casts to payload data should be made
+   using support_fuse_in_cast.
+
+   By default, FUSE_FORGET responses are filtered.  See
+   support_fuse_filter_forget for turning that off.  */
+struct fuse_in_header *support_fuse_next (struct support_fuse *f)
+  __nonnull ((1));
+
+/* This function can be called from a callback function to handle
+   basic aspects of directories (OPENDIR, GETATTR, RELEASEDIR).
+   inh->nodeid is used as the inode number for the directory.  This
+   function must be called after support_fuse_next.  */
+bool support_fuse_handle_directory (struct support_fuse *f) __nonnull ((1));
+
+/* This function can be called from a callback function to handle
+   access to the mount point itself, after call support_fuse_next.  */
+bool support_fuse_handle_mountpoint (struct support_fuse *f) __nonnull ((1));
+
+/* If FILTER_ENABLED, future support_fuse_next calls will not return
+   FUSE_FORGET events (and simply discared them, as they require no
+   reply).  If !FILTER_ENABLED, the callback needs to handle
+   FUSE_FORGET events and call support_fuse_no_reply.  */
+void support_fuse_filter_forget (struct support_fuse *f, bool filter_enabled)
+  __nonnull ((1));
+
+/* This function should be called from the callback function after
+   support_fuse_next returned a non-null pointer.  It sends out a
+   response packet on the FUSE device with the supplied payload data.  */
+void support_fuse_reply (struct support_fuse *f,
+                         const void *payload, size_t payload_size)
+  __nonnull ((1)) __attr_access ((__read_only__, 2, 3));
+
+/* This function should be called from the callback function.  It
+   replies to a request with an error indicator.  ERROR must be positive.  */
+void support_fuse_reply_error (struct support_fuse *f, uint32_t error)
+    __nonnull ((1));
+
+/* This function should be called from the callback function.  It
+   sends out an empty (but success-indicating) reply packet.  */
+void support_fuse_reply_empty (struct support_fuse *f) __nonnull ((1));
+
+/* Do not send a reply.  Only to be used after a support_fuse_next
+   call that returned a FUSE_FORGET event.  */
+void support_fuse_no_reply (struct support_fuse *f) __nonnull ((1));
+
+/* Specific reponse preparation functions.  The returned object can be
+   updated as needed.  If a NODEID argument is present, it will be
+   used to set the inode and FUSE nodeid fields.  Without such an
+   argument, it is initialized from the current request (if the reply
+   requires this field).  This function must be called after
+   support_fuse_next.  The actual response must be sent using
+   support_fuse_reply_prepared (or a support_fuse_reply_error call can
+   be used to cancel the response).  */
+struct fuse_entry_out *support_fuse_prepare_entry (struct support_fuse *f,
+                                                   uint64_t nodeid)
+  __nonnull ((1));
+struct fuse_attr_out *support_fuse_prepare_attr (struct support_fuse *f)
+  __nonnull ((1));
+
+/* Similar to the other support_fuse_prepare_* functions, but it
+   prepares for two response packets.  They can be updated through the
+   pointers written to *OUT_ENTRY and *OUT_OPEN prior to calling
+   support_fuse_reply_prepared.  */
+void support_fuse_prepare_create (struct support_fuse *f,
+                                  uint64_t nodeid,
+                                  struct fuse_entry_out **out_entry,
+                                  struct fuse_open_out **out_open)
+  __nonnull ((1, 3, 4));
+
+
+/* Prepare sending a directory stream.  Must be called after
+   support_fuse_next and before support_fuse_dirstream_add.    */
+struct support_fuse_dirstream;
+struct support_fuse_dirstream *support_fuse_prepare_readdir (struct
+                                                             support_fuse *f);
+
+/* Adds directory using D_INO, D_OFF, D_TYPE, D_NAME to the directory
+   stream D.  Must be called after support_fuse_prepare_readdir.
+
+   D_OFF is the offset of the next directory entry, not the current
+   one.  The first entry has offset zero.  The first requested offset
+   can be obtained from the READ payload (struct fuse_read_in) prior
+   to calling this function.
+
+   Returns true if the entry could be added to the buffer, or false if
+   there was insufficient room.  Sending the buffer is delayed until
+   support_fuse_reply_prepared is called.  */
+bool support_fuse_dirstream_add (struct support_fuse_dirstream *d,
+                                 uint64_t d_ino, uint64_t d_off,
+                                 uint32_t d_type,
+                                 const char *d_name);
+
+/* Send a prepared response.  Must be called after one of the
+   support_fuse_prepare_* functions and before the next
+   support_fuse_next call.  */
+void support_fuse_reply_prepared (struct support_fuse *f) __nonnull ((1));
+
+#endif /* SUPPORT_FUSE_H */
diff --git a/support/support_fuse.c b/support/support_fuse.c
new file mode 100644
index 0000000000..135dbf1198
--- /dev/null
+++ b/support/support_fuse.c
@@ -0,0 +1,705 @@
+/* Facilities for FUSE-backed file system tests.
+   Copyright (C) 2024 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <support/fuse.h>
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <sys/sysmacros.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+#include <array_length.h>
+#include <support/check.h>
+#include <support/namespace.h>
+#include <support/support.h>
+#include <support/test-driver.h>
+#include <support/xdirent.h>
+#include <support/xthread.h>
+#include <support/xunistd.h>
+
+#ifdef __linux__
+# include <sys/mount.h>
+#else
+/* Fallback definitions that mark the test as unsupported.  */
+# define mount(...) ({ FAIL_UNSUPPORTED ("mount"); -1; })
+# define umount(...) ({ FAIL_UNSUPPORTED ("mount"); -1; })
+#endif
+
+struct support_fuse
+{
+  char *mountpoint;
+  void *buffer_start;           /* Begin of allocation.  */
+  void *buffer_next;            /* Next read position.  */
+  void *buffer_limit;           /* End of buffered data.  */
+  void *buffer_end;             /* End of allocation.  */
+  struct fuse_in_header *inh;   /* Most recent request (support_fuse_next).  */
+  union                         /* Space for prepared responses.  */
+  {
+    struct fuse_attr_out attr;
+    struct fuse_entry_out entry;
+    struct
+    {
+      struct fuse_entry_out entry;
+      struct fuse_open_out open;
+    } create;
+  } prepared;
+  void *prepared_pointer;       /* NULL if inactive.  */
+  size_t prepared_size;         /* 0 if inactive.  */
+
+  /* Used for preparing readdir responses.  Already used-up area for
+     the current request is counted by prepared_size.  */
+  void *readdir_buffer;
+  size_t readdir_buffer_size;
+
+  pthread_t handler;            /* Thread handling requests.  */
+  uid_t uid;                    /* Cached value for the current process.  */
+  uid_t gid;                    /* Cached value for the current process.  */
+  int fd;                       /* FUSE file descriptor.  */
+  int connection;               /* Entry under /sys/fs/fuse/connections.  */
+  bool filter_forget;           /* Controls FUSE_FORGET event dropping.  */
+  _Atomic bool disconnected;
+};
+
+struct fuse_thread_wrapper_args
+{
+  struct support_fuse *f;
+  support_fuse_callback callback;
+  void *closure;
+};
+
+/* Set by support_fuse_init to indicate that support_fuse_mount may be
+   called.  */
+static bool support_fuse_init_called;
+
+/* Allocate the read buffer in F with SIZE bytes capacity.  Does not
+   free the previously allocated buffer.  */
+static void support_fuse_allocate (struct support_fuse *f, size_t size)
+  __nonnull ((1));
+
+/* Internal mkdtemp replacement */
+static char * support_fuse_mkdir (const char *prefix) __nonnull ((1));
+
+/* Low-level allocation function for support_fuse_mount.  Does not
+   perform the mount.  */
+static struct support_fuse *support_fuse_open (void);
+
+/* Thread wrapper function for use with pthread_create.  Uses struct
+   fuse_thread_wrapper_args.  */
+static void *support_fuse_thread_wrapper (void *closure)  __nonnull ((1));
+
+/* Initial step before preparing a reply.  SIZE must be the size of
+   the F->prepared member that is going to be used.  */
+static void support_fuse_prepare_1 (struct support_fuse *f, size_t size);
+
+/* Similar to support_fuse_reply_error, but not check that ERROR is
+   not zero.  */
+static void support_fuse_reply_error_1 (struct support_fuse *f,
+                                        uint32_t error) __nonnull ((1));
+
+/* Path to the directory containing mount points.  Initialized by an
+   ELF constructor.  All mountpoints are collected there so that the
+   test wrapper can clean them up without keeping track of them
+   individually.  */
+static char *support_fuse_mountpoints;
+
+/* PID of the process that should clean up the mount points in the ELF
+   destructor.  */
+static pid_t support_fuse_cleanup_pid;
+
+static void
+support_fuse_allocate (struct support_fuse *f, size_t size)
+{
+  f->buffer_start = xmalloc (size);
+  f->buffer_end = f->buffer_start + size;
+  f->buffer_limit = f->buffer_start;
+  f->buffer_next = f->buffer_limit;
+}
+
+void
+support_fuse_filter_forget (struct support_fuse *f, bool filter)
+{
+  f->filter_forget = filter;
+}
+
+void *
+support_fuse_cast_internal (struct fuse_in_header *p, uint32_t expected)
+{
+  if (expected != p->opcode
+      && !(expected == FUSE_READ && p->opcode == FUSE_READDIR))
+    {
+      char *expected1 = support_fuse_opcode (expected);
+      char *actual = support_fuse_opcode (p->opcode);
+      FAIL_EXIT1 ("attempt to cast %s to %s", actual, expected1);
+    }
+  return p + 1;
+}
+
+void *
+support_fuse_cast_name_internal (struct fuse_in_header *p, uint32_t expected,
+                                 size_t skip, char **name)
+{
+  char *result = support_fuse_cast_internal (p, expected);
+  *name = result + skip;
+  return result;
+}
+
+bool
+support_fuse_dirstream_add (struct support_fuse_dirstream *d,
+                            uint64_t d_ino, uint64_t d_off,
+                            uint32_t d_type, const char *d_name)
+{
+  struct support_fuse *f = (struct support_fuse *) d;
+  size_t structlen = offsetof (struct fuse_dirent, name);
+  size_t namelen = strlen (d_name); /* No null termination.  */
+  size_t required_size = FUSE_DIRENT_ALIGN (structlen + namelen);
+  if (f->readdir_buffer_size - f->prepared_size < required_size)
+    return false;
+  struct fuse_dirent entry =
+    {
+      .ino = d_ino,
+      .off = d_off,
+      .type = d_type,
+      .namelen = namelen,
+    };
+  memcpy (f->readdir_buffer + f->prepared_size, &entry, structlen);
+  /* Use strncpy to write padding and avoid passing uninitialized
+     bytes to the read system call.  */
+  strncpy (f->readdir_buffer + f->prepared_size + structlen, d_name,
+           required_size - structlen);
+  f->prepared_size += required_size;
+  return true;
+}
+
+bool
+support_fuse_handle_directory (struct support_fuse *f)
+{
+  TEST_VERIFY (f->inh != NULL);
+  switch (f->inh->opcode)
+    {
+    case FUSE_OPENDIR:
+      {
+        struct fuse_open_out out =
+          {
+          };
+        support_fuse_reply (f, &out, sizeof (out));
+      }
+      return true;
+    case FUSE_RELEASEDIR:
+      support_fuse_reply_empty (f);
+      return true;
+    case FUSE_GETATTR:
+      {
+        struct fuse_attr_out *out = support_fuse_prepare_attr (f);
+        out->attr.mode = S_IFDIR | 0700;
+        support_fuse_reply_prepared (f);
+      }
+      return true;
+    default:
+      return false;
+    }
+}
+
+bool
+support_fuse_handle_mountpoint (struct support_fuse *f)
+{
+  TEST_VERIFY (f->inh != NULL);
+  /* 1 is the root node.  */
+  if (f->inh->opcode == FUSE_GETATTR && f->inh->nodeid == 1)
+    return support_fuse_handle_directory (f);
+  return false;
+}
+
+void
+support_fuse_init (void)
+{
+  support_fuse_init_called = true;
+
+  support_become_root ();
+  if (!support_enter_mount_namespace ())
+    FAIL_UNSUPPORTED ("mount namespaces not supported");
+}
+
+void
+support_fuse_init_no_namespace (void)
+{
+  support_fuse_init_called = true;
+}
+
+static char *
+support_fuse_mkdir (const char *prefix)
+{
+  /* Do not use mkdtemp to avoid interfering with its tests.  */
+  unsigned int counter = 1;
+  unsigned int pid = getpid ();
+  while (true)
+    {
+      char *path = xasprintf ("%s%u.%u/", prefix, pid, counter);
+      if (mkdir (path, 0700) == 0)
+        return path;
+      if (errno != EEXIST)
+        FAIL_EXIT1 ("mkdir (\"%s\"): %m", path);
+      free (path);
+      ++counter;
+    }
+}
+
+struct support_fuse *
+support_fuse_mount (support_fuse_callback callback, void *closure)
+{
+  TEST_VERIFY_EXIT (support_fuse_init_called);
+
+  /* Request at least minor version 12 because it changed struct sizes.  */
+  enum { min_version = 12 };
+
+  struct support_fuse *f = support_fuse_open ();
+  char *mount_options
+    = xasprintf ("fd=%d,rootmode=040700,user_id=%u,group_id=%u",
+                 f->fd, f->uid, f->gid);
+  if (mount ("fuse", f->mountpoint, "fuse.glibc",
+             MS_NOSUID|MS_NODEV, mount_options)
+      != 0)
+    FAIL_EXIT1 ("FUSE mount on %s: %m", f->mountpoint);
+  free (mount_options);
+
+  /* Retry with an older FUSE version.  */
+  while (true)
+    {
+      struct fuse_in_header *inh = support_fuse_next (f);
+      struct fuse_init_in *init_in = support_fuse_cast (INIT, inh);
+      if (init_in->major < 7
+          || (init_in->major == 7 && init_in->minor < min_version))
+        FAIL_UNSUPPORTED ("kernel FUSE version is %u.%u, too old",
+                          init_in->major, init_in->minor);
+      if (init_in->major > 7)
+        {
+          uint32_t major = 7;
+          support_fuse_reply (f, &major, sizeof (major));
+          continue;
+        }
+      TEST_VERIFY (init_in->flags & FUSE_DONT_MASK);
+      struct fuse_init_out out =
+        {
+          .major = 7,
+          .minor = min_version,
+          /* Request that the kernel does not apply umask.  */
+          .flags = FUSE_DONT_MASK,
+        };
+      support_fuse_reply (f, &out, sizeof (out));
+
+      {
+        struct fuse_thread_wrapper_args args =
+          {
+            .f = f,
+            .callback = callback,
+            .closure = closure,
+          };
+        f->handler = xpthread_create (NULL,
+                                      support_fuse_thread_wrapper, &args);
+        struct stat64 st;
+        xstat64 (f->mountpoint, &st);
+        f->connection = minor (st.st_dev);
+        /* Got a reply from the thread,  safe to deallocate args.  */
+      }
+
+      return f;
+    }
+}
+
+const char *
+support_fuse_mountpoint (struct support_fuse *f)
+{
+  return f->mountpoint;
+}
+
+void
+support_fuse_no_reply (struct support_fuse *f)
+{
+  TEST_VERIFY (f->inh != NULL);
+  TEST_COMPARE (f->inh->opcode, FUSE_FORGET);
+  f->inh = NULL;
+}
+
+char *
+support_fuse_opcode (uint32_t op)
+{
+  const char *result;
+  switch (op)
+    {
+#define X(n) case n: result = #n; break
+      X(FUSE_LOOKUP);
+      X(FUSE_FORGET);
+      X(FUSE_GETATTR);
+      X(FUSE_SETATTR);
+      X(FUSE_READLINK);
+      X(FUSE_SYMLINK);
+      X(FUSE_MKNOD);
+      X(FUSE_MKDIR);
+      X(FUSE_UNLINK);
+      X(FUSE_RMDIR);
+      X(FUSE_RENAME);
+      X(FUSE_LINK);
+      X(FUSE_OPEN);
+      X(FUSE_READ);
+      X(FUSE_WRITE);
+      X(FUSE_STATFS);
+      X(FUSE_RELEASE);
+      X(FUSE_FSYNC);
+      X(FUSE_SETXATTR);
+      X(FUSE_GETXATTR);
+      X(FUSE_LISTXATTR);
+      X(FUSE_REMOVEXATTR);
+      X(FUSE_FLUSH);
+      X(FUSE_INIT);
+      X(FUSE_OPENDIR);
+      X(FUSE_READDIR);
+      X(FUSE_RELEASEDIR);
+      X(FUSE_FSYNCDIR);
+      X(FUSE_GETLK);
+      X(FUSE_SETLK);
+      X(FUSE_SETLKW);
+      X(FUSE_ACCESS);
+      X(FUSE_CREATE);
+      X(FUSE_INTERRUPT);
+      X(FUSE_BMAP);
+      X(FUSE_DESTROY);
+      X(FUSE_IOCTL);
+      X(FUSE_POLL);
+      X(FUSE_NOTIFY_REPLY);
+      X(FUSE_BATCH_FORGET);
+      X(FUSE_FALLOCATE);
+      X(FUSE_READDIRPLUS);
+      X(FUSE_RENAME2);
+      X(FUSE_LSEEK);
+      X(FUSE_COPY_FILE_RANGE);
+      X(FUSE_SETUPMAPPING);
+      X(FUSE_REMOVEMAPPING);
+      X(FUSE_SYNCFS);
+      X(FUSE_TMPFILE);
+      X(FUSE_STATX);
+#undef X
+    default:
+      return xasprintf ("FUSE_unknown_%u", op);
+    }
+  return xstrdup (result);
+}
+
+static struct support_fuse *
+support_fuse_open (void)
+{
+  struct support_fuse *result = xmalloc (sizeof (*result));
+  result->mountpoint = support_fuse_mkdir (support_fuse_mountpoints);
+  result->inh = NULL;
+  result->prepared_pointer = NULL;
+  result->prepared_size = 0;
+  result->readdir_buffer = NULL;
+  result->readdir_buffer_size = 0;
+  result->uid = getuid ();
+  result->gid = getgid ();
+  result->fd = open ("/dev/fuse", O_RDWR, 0);
+  if (result->fd < 0)
+    {
+      if (errno == ENOENT || errno == ENODEV || errno == EPERM
+          || errno == EACCES)
+        FAIL_UNSUPPORTED ("cannot open /dev/fuse: %m");
+      else
+        FAIL_EXIT1 ("cannot open /dev/fuse: %m");
+    }
+  result->connection = -1;
+  result->filter_forget = true;
+  result->disconnected = false;
+  support_fuse_allocate (result, FUSE_MIN_READ_BUFFER);
+  return result;
+}
+
+static void
+support_fuse_prepare_1 (struct support_fuse *f, size_t size)
+{
+  TEST_VERIFY (f->prepared_pointer == NULL);
+  f->prepared_size = size;
+  memset (&f->prepared, 0, size);
+  f->prepared_pointer = &f->prepared;
+}
+
+struct fuse_attr_out *
+support_fuse_prepare_attr (struct support_fuse *f)
+{
+  support_fuse_prepare_1 (f, sizeof (f->prepared.attr));
+  f->prepared.attr.attr.uid = f->uid;
+  f->prepared.attr.attr.gid = f->gid;
+  f->prepared.attr.attr.ino = f->inh->nodeid;
+  return &f->prepared.attr;
+}
+
+void
+support_fuse_prepare_create (struct support_fuse *f,
+                             uint64_t nodeid,
+                             struct fuse_entry_out **out_entry,
+                             struct fuse_open_out **out_open)
+{
+  support_fuse_prepare_1 (f, sizeof (f->prepared.create));
+  f->prepared.create.entry.nodeid = nodeid;
+  f->prepared.create.entry.attr.uid = f->uid;
+  f->prepared.create.entry.attr.gid = f->gid;
+  f->prepared.create.entry.attr.ino = nodeid;
+  *out_entry = &f->prepared.create.entry;
+  *out_open = &f->prepared.create.open;
+}
+
+struct fuse_entry_out *
+support_fuse_prepare_entry (struct support_fuse *f, uint64_t nodeid)
+{
+  support_fuse_prepare_1 (f, sizeof (f->prepared.entry));
+  f->prepared.entry.nodeid = nodeid;
+  f->prepared.entry.attr.uid = f->uid;
+  f->prepared.entry.attr.gid = f->gid;
+  f->prepared.entry.attr.ino = nodeid;
+  return &f->prepared.entry;
+}
+
+struct support_fuse_dirstream *
+support_fuse_prepare_readdir (struct support_fuse *f)
+{
+  support_fuse_prepare_1 (f, 0);
+  struct fuse_read_in *p = support_fuse_cast (READ, f->inh);
+  if (p->size > f->readdir_buffer_size)
+    {
+      free (f->readdir_buffer);
+      f->readdir_buffer = xmalloc (p->size);
+      f->readdir_buffer_size = p->size;
+    }
+  f->prepared_pointer = f->readdir_buffer;
+  return (struct support_fuse_dirstream *) f;
+}
+
+struct fuse_in_header *
+support_fuse_next (struct support_fuse *f)
+{
+  TEST_VERIFY (f->inh == NULL);
+  while (true)
+    {
+      if (f->buffer_next < f->buffer_limit)
+        {
+          f->inh = f->buffer_next;
+          f->buffer_next = (void *) f->buffer_next + f->inh->len;
+          /* Suppress FUSE_FORGET responses if requested.  */
+          if (f->filter_forget && f->inh->opcode == FUSE_FORGET)
+            {
+              f->inh = NULL;
+              continue;
+            }
+          return f->inh;
+        }
+      ssize_t ret = read (f->fd, f->buffer_start,
+                          f->buffer_end - f->buffer_start);
+      if (ret == 0)
+        FAIL_EXIT (1, "unexpected EOF on FUSE device");
+      if (ret < 0 && errno == EINVAL)
+        {
+          /* Increase buffer size.  */
+          size_t new_size = 2 * (size_t) (f->buffer_end - f->buffer_start);
+          free (f->buffer_start);
+          support_fuse_allocate (f, new_size);
+          continue;
+        }
+      if (ret < 0)
+        {
+          if (f->disconnected)
+            /* Unmount detected.  */
+            return NULL;
+          FAIL_EXIT1 ("read error on FUSE device: %m");
+        }
+      /* Read was successful, make [next, limit) the active buffer area.  */
+      f->buffer_next = f->buffer_start;
+      f->buffer_limit = (void *) f->buffer_start + ret;
+    }
+}
+
+void
+support_fuse_reply (struct support_fuse *f,
+                    const void *payload, size_t payload_size)
+{
+  TEST_VERIFY_EXIT (f->inh != NULL);
+  TEST_VERIFY (f->prepared_pointer == NULL);
+  struct fuse_out_header outh =
+    {
+      .len = sizeof (outh) + payload_size,
+      .unique = f->inh->unique,
+    };
+  struct iovec iov[] =
+    {
+      { &outh, sizeof (outh) },
+      { (void *) payload, payload_size },
+    };
+  ssize_t ret = writev (f->fd, iov, array_length (iov));
+  if (ret < 0)
+    {
+      if (!f->disconnected)
+        /* Some kernels produce write errors upon disconnect.  */
+        FAIL_EXIT1 ("FUSE write failed for %s response"
+                    " (%zu bytes payload): %m",
+                    support_fuse_opcode (f->inh->opcode), payload_size);
+    }
+  else if (ret != sizeof (outh) + payload_size)
+    FAIL_EXIT1 ("FUSE write short for %s response (%zu bytes payload):"
+                " %zd bytes",
+                support_fuse_opcode (f->inh->opcode), payload_size, ret);
+  f->inh = NULL;
+}
+
+void
+support_fuse_reply_empty (struct support_fuse *f)
+{
+  support_fuse_reply_error_1 (f, 0);
+}
+
+static void
+support_fuse_reply_error_1 (struct support_fuse *f, uint32_t error)
+{
+  TEST_VERIFY_EXIT (f->inh != NULL);
+  struct fuse_out_header outh =
+    {
+      .len = sizeof (outh),
+      .error = -error,
+      .unique = f->inh->unique,
+    };
+  ssize_t ret = write (f->fd, &outh, sizeof (outh));
+  if (ret < 0)
+    {
+      /* Some kernels produce write errors upon disconnect.  */
+      if (!f->disconnected)
+        FAIL_EXIT1 ("FUSE write failed for %s error response: %m",
+                    support_fuse_opcode (f->inh->opcode));
+    }
+  else if (ret != sizeof (outh))
+    FAIL_EXIT1 ("FUSE write short for %s error response: %zd bytes",
+                support_fuse_opcode (f->inh->opcode), ret);
+  f->inh = NULL;
+  f->prepared_pointer = NULL;
+  f->prepared_size = 0;
+}
+
+void
+support_fuse_reply_error (struct support_fuse *f, uint32_t error)
+{
+  TEST_VERIFY (error > 0);
+  support_fuse_reply_error_1 (f, error);
+}
+
+void
+support_fuse_reply_prepared (struct support_fuse *f)
+{
+  TEST_VERIFY_EXIT (f->prepared_pointer != NULL);
+  /* Re-use the non-prepared reply function.  It requires
+     f->prepared_* to be non-null, so reset the fields before the call.  */
+  void *prepared_pointer = f->prepared_pointer;
+  size_t prepared_size = f->prepared_size;
+  f->prepared_pointer = NULL;
+  f->prepared_size = 0;
+  support_fuse_reply (f, prepared_pointer, prepared_size);
+}
+
+static void *
+support_fuse_thread_wrapper (void *closure)
+{
+  struct fuse_thread_wrapper_args args
+    = *(struct fuse_thread_wrapper_args *) closure;
+
+  /* Handle the initial stat call.  */
+  struct fuse_in_header *inh = support_fuse_next (args.f);
+  if (inh == NULL || !support_fuse_handle_mountpoint (args.f))
+    {
+      support_fuse_reply_error (args.f, EIO);
+      return NULL;
+    }
+
+  args.callback  (args.f, args.closure);
+  return NULL;
+}
+
+void
+support_fuse_unmount (struct support_fuse *f)
+{
+  /* Signal the unmount to the handler thread.  Some kernels report
+     not just ENODEV errors on read.  */
+  f->disconnected = true;
+
+  {
+    char *path = xasprintf ("/sys/fs/fuse/connections/%d/abort",
+                            f->connection);
+    /* Some kernels do not support these files under /sys.  */
+    int fd = open (path, O_RDWR | O_TRUNC);
+    if (fd >= 0)
+      {
+        TEST_COMPARE (write (fd, "1", 1), 1);
+        xclose (fd);
+      }
+    free (path);
+  }
+  if (umount (f->mountpoint) != 0)
+    FAIL ("FUSE: umount (\"%s\"): %m", f->mountpoint);
+  xpthread_join (f->handler);
+  if (rmdir (f->mountpoint) != 0)
+    FAIL ("FUSE: rmdir (\"%s\"): %m", f->mountpoint);
+  xclose (f->fd);
+  free (f->mountpoint);
+  free (f->readdir_buffer);
+  free (f);
+}
+
+static void __attribute__ ((constructor))
+init (void)
+{
+  /* The test_dir test driver variable is not yet set at this point.  */
+  const char *tmpdir = getenv ("TMPDIR");
+  if (tmpdir == NULL || tmpdir[0] == '\0')
+    tmpdir = "/tmp";
+
+  char *prefix = xasprintf ("%s/glibc-tst-fuse.", tmpdir);
+  support_fuse_mountpoints = support_fuse_mkdir (prefix);
+  free (prefix);
+  support_fuse_cleanup_pid = getpid ();
+}
+
+static void __attribute__ ((destructor))
+fini (void)
+{
+  if (support_fuse_cleanup_pid != getpid ()
+      || support_fuse_mountpoints == NULL)
+    return;
+  DIR *dir = xopendir (support_fuse_mountpoints);
+  while (true)
+    {
+      struct dirent64 *e = readdir64 (dir);
+      if (e == NULL)
+        /* Ignore errors.  */
+        break;
+      if (*e->d_name == '.')
+        /* Skip "." and "..".  No hidden files expected.  */
+        continue;
+      if (unlinkat (dirfd (dir), e->d_name, AT_REMOVEDIR) != 0)
+        break;
+      rewinddir (dir);
+    }
+  xclosedir (dir);
+  rmdir (support_fuse_mountpoints);
+  free (support_fuse_mountpoints);
+  support_fuse_mountpoints = NULL;
+}
diff --git a/support/tst-support_fuse.c b/support/tst-support_fuse.c
new file mode 100644
index 0000000000..c4075a6608
--- /dev/null
+++ b/support/tst-support_fuse.c
@@ -0,0 +1,348 @@
+/* Facilities for FUSE-backed file system tests.
+   Copyright (C) 2024 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <support/fuse.h>
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/support.h>
+#include <support/xdirent.h>
+#include <support/xunistd.h>
+
+static void
+fuse_thread (struct support_fuse *f, void *closure)
+{
+  /* Turn on returning FUSE_FORGET responses.  */
+  support_fuse_filter_forget (f, false);
+
+  /* Inode and nodeid for "file" and "new".  */
+  enum { NODE_FILE = 2, NODE_NEW, NODE_SUBDIR, NODE_SYMLINK };
+  struct fuse_in_header *inh;
+  while ((inh = support_fuse_next (f)) != NULL)
+    {
+      {
+        char *opcode = support_fuse_opcode (inh->opcode);
+        printf ("info: (T) event %s(%llu) len=%u nodeid=%llu\n",
+                opcode, (unsigned long long int) inh->unique, inh->len,
+                (unsigned long long int) inh->nodeid);
+        free (opcode);
+      }
+
+      /* Handle mountpoint and basic directory operation for the root (1).  */
+      if (support_fuse_handle_mountpoint (f)
+          || (inh->nodeid == 1 && support_fuse_handle_directory (f)))
+        continue;
+
+      switch (inh->opcode)
+        {
+        case FUSE_READDIR:
+          /* Implementation of getdents64.  */
+          if (inh->nodeid == 1)
+            {
+              struct support_fuse_dirstream *d
+                = support_fuse_prepare_readdir (f);
+              TEST_COMPARE (support_fuse_cast (READ, inh)->offset, 0);
+              TEST_VERIFY (support_fuse_dirstream_add (d, 1, 1, DT_DIR, "."));
+              TEST_VERIFY (support_fuse_dirstream_add (d, 1, 2, DT_DIR, ".."));
+              TEST_VERIFY (support_fuse_dirstream_add (d, NODE_FILE, 3, DT_REG,
+                                                       "file"));
+              support_fuse_reply_prepared (f);
+            }
+          else
+            support_fuse_reply_error (f, EIO);
+          break;
+        case FUSE_LOOKUP:
+          /* Part of the implementation of open.  */
+          {
+            char *name = support_fuse_cast (LOOKUP, inh);
+            printf ("  name: %s\n", name);
+            if (inh->nodeid == 1 && strcmp (name, "file") == 0)
+              {
+                struct fuse_entry_out *out
+                  = support_fuse_prepare_entry (f, NODE_FILE);
+                out->attr.mode = S_IFREG | 0600;
+                support_fuse_reply_prepared (f);
+              }
+            else if (inh->nodeid == 1 && strcmp (name, "symlink") == 0)
+              {
+                struct fuse_entry_out *out
+                  = support_fuse_prepare_entry (f, NODE_SYMLINK);
+                out->attr.mode = S_IFLNK | 0777;
+                support_fuse_reply_prepared (f);
+              }
+            else
+              support_fuse_reply_error (f, ENOENT);
+          }
+          break;
+        case FUSE_OPEN:
+          /* Implementation of open.  */
+          {
+            struct fuse_open_in *p = support_fuse_cast (OPEN, inh);
+            if (inh->nodeid == NODE_FILE)
+              {
+                TEST_VERIFY (!(p->flags & O_EXCL));
+                struct fuse_open_out out = { 0, };
+                support_fuse_reply (f, &out, sizeof (out));
+              }
+            else
+              support_fuse_reply_error (f, ENOENT);
+          }
+          break;
+        case FUSE_GETATTR:
+          /* Happens after open.  */
+          if (inh->nodeid == NODE_FILE)
+            {
+              struct fuse_attr_out *out = support_fuse_prepare_attr (f);
+              out->attr.mode = S_IFREG | 0600;
+              out->attr.size = strlen ("Hello, world!");
+              support_fuse_reply_prepared (f);
+            }
+          else
+            support_fuse_reply_error (f, ENOENT);
+          break;
+        case FUSE_READ:
+          /* Implementation of read.  */
+          if (inh->nodeid == NODE_FILE)
+            {
+              struct fuse_read_in *p = support_fuse_cast (READ, inh);
+              TEST_COMPARE (p->offset, 0);
+              TEST_VERIFY (p->size >= strlen ("Hello, world!"));
+              support_fuse_reply (f,
+                                  "Hello, world!", strlen ("Hello, world!"));
+            }
+          else
+            support_fuse_reply_error (f, EIO);
+          break;
+        case FUSE_FLUSH:
+          /* Sent in response to close.  */
+          support_fuse_reply_empty (f);
+          break;
+        case FUSE_GETXATTR:
+          /* This happens as part of a open-for-write operation.
+             Signal no support for extended attributes.  */
+          support_fuse_reply_error (f, ENOSYS);
+          break;
+        case FUSE_SETATTR:
+          /* This happens as part of a open-for-write operation to
+             implement O_TRUNC.  */
+          if (inh->nodeid == NODE_FILE)
+            {
+              struct fuse_setattr_in *p = support_fuse_cast (SETATTR, inh);
+              /* FATTR_LOCKOWNER may also be set.  */
+              TEST_COMPARE ((p->valid) & ~ FATTR_LOCKOWNER, FATTR_SIZE);
+              TEST_COMPARE (p->size, 0);
+              struct fuse_attr_out *out = support_fuse_prepare_attr (f);
+              out->attr.mode = S_IFREG | 0600;
+              support_fuse_reply_prepared (f);
+            }
+          else
+            support_fuse_reply_error (f, EIO);
+          break;
+        case FUSE_WRITE:
+          /* Implementation of write.  */
+          if (inh->nodeid == NODE_FILE)
+            {
+              struct fuse_write_in *p = support_fuse_cast (WRITE, inh);
+              TEST_COMPARE (p->offset, 0);
+              /* Write payload follows after struct fuse_write_in.  */
+              TEST_COMPARE_BLOB (p + 1, p->size,
+                                 "Good day to you too.",
+                                 strlen ("Good day to you too."));
+              struct fuse_write_out out =
+                {
+                  .size = p->size,
+                };
+              support_fuse_reply (f, &out, sizeof (out));
+            }
+          else
+            support_fuse_reply_error (f, EIO);
+          break;
+        case FUSE_CREATE:
+          /* Implementation of O_CREAT.  */
+          if (inh->nodeid == 1)
+            {
+              char *name;
+              struct fuse_create_in *p
+                = support_fuse_cast_name (CREATE, inh, &name);
+              TEST_VERIFY (S_ISREG (p->mode));
+              TEST_COMPARE (p->mode & 07777, 0600);
+              TEST_COMPARE_STRING (name, "new");
+              struct fuse_entry_out *out_entry;
+              struct fuse_open_out *out_open;
+              support_fuse_prepare_create (f, NODE_NEW, &out_entry, &out_open);
+              out_entry->attr.mode = S_IFREG | 0600;
+              support_fuse_reply_prepared (f);
+            }
+          else
+            support_fuse_reply_error (f, EIO);
+          break;
+        case FUSE_MKDIR:
+          /* Implementation of mkdir.  */
+          {
+            if (inh->nodeid == 1)
+              {
+                char *name;
+                struct fuse_mkdir_in *p
+                  = support_fuse_cast_name (MKDIR, inh, &name);
+                TEST_COMPARE (p->mode, 01234);
+                TEST_COMPARE_STRING (name, "subdir");
+                struct fuse_entry_out *out
+                  = support_fuse_prepare_entry (f, NODE_SUBDIR);
+                out->attr.mode = S_IFDIR | p->mode;
+                support_fuse_reply_prepared (f);
+              }
+            else
+              support_fuse_reply_error (f, EIO);
+          }
+          break;
+        case FUSE_READLINK:
+          /* Implementation of readlink.  */
+          TEST_COMPARE (inh->nodeid, NODE_SYMLINK);
+          if (inh->nodeid == NODE_SYMLINK)
+            support_fuse_reply (f, "target-of-symbolic-link",
+                                strlen ("target-of-symbolic-link"));
+          else
+            support_fuse_reply_error (f, EINVAL);
+          break;
+        case FUSE_FORGET:
+          support_fuse_no_reply (f);
+          break;
+        default:
+          support_fuse_reply_error (f, EIO);
+        }
+    }
+}
+
+static int
+do_test (void)
+{
+  support_fuse_init ();
+
+  struct support_fuse *f = support_fuse_mount (fuse_thread, NULL);
+
+  printf ("info: Attributes of mountpoint/root directory %s\n",
+          support_fuse_mountpoint (f));
+  {
+    struct statx st;
+    xstatx (AT_FDCWD, support_fuse_mountpoint (f), 0, STATX_BASIC_STATS, &st);
+    TEST_COMPARE (st.stx_uid, getuid ());
+    TEST_COMPARE (st.stx_gid, getgid ());
+    TEST_VERIFY (S_ISDIR (st.stx_mode));
+    TEST_COMPARE (st.stx_mode & 07777, 0700);
+  }
+
+  printf ("info: List directory %s\n", support_fuse_mountpoint (f));
+  {
+    DIR *dir = xopendir (support_fuse_mountpoint (f));
+
+    struct dirent *e = xreaddir (dir);
+    TEST_COMPARE (e->d_ino, 1);
+#ifdef _DIRENT_HAVE_D_OFF
+    TEST_COMPARE (e->d_off, 1);
+#endif
+    TEST_COMPARE (e->d_type, DT_DIR);
+    TEST_COMPARE_STRING (e->d_name, ".");
+
+    e = xreaddir (dir);
+    TEST_COMPARE (e->d_ino, 1);
+#ifdef _DIRENT_HAVE_D_OFF
+    TEST_COMPARE (e->d_off, 2);
+#endif
+    TEST_COMPARE (e->d_type, DT_DIR);
+    TEST_COMPARE_STRING (e->d_name, "..");
+
+    e = xreaddir (dir);
+    TEST_COMPARE (e->d_ino, 2);
+#ifdef _DIRENT_HAVE_D_OFF
+    TEST_COMPARE (e->d_off, 3);
+#endif
+    TEST_COMPARE (e->d_type, DT_REG);
+    TEST_COMPARE_STRING (e->d_name, "file");
+
+    TEST_COMPARE (closedir (dir), 0);
+  }
+
+  char *file_path = xasprintf ("%s/file", support_fuse_mountpoint (f));
+
+  printf ("info: Attributes of file %s\n", file_path);
+  {
+    struct statx st;
+    xstatx (AT_FDCWD, file_path, 0, STATX_BASIC_STATS, &st);
+    TEST_COMPARE (st.stx_uid, getuid ());
+    TEST_COMPARE (st.stx_gid, getgid ());
+    TEST_VERIFY (S_ISREG (st.stx_mode));
+    TEST_COMPARE (st.stx_mode & 07777, 0600);
+    TEST_COMPARE (st.stx_size, strlen ("Hello, world!"));
+  }
+
+  printf ("info: Read from %s\n", file_path);
+  {
+    int fd = xopen (file_path, O_RDONLY, 0);
+    char buf[64];
+    ssize_t len = read (fd, buf, sizeof (buf));
+    if (len < 0)
+      FAIL_EXIT1 ("read: %m");
+    TEST_COMPARE_BLOB (buf, len, "Hello, world!", strlen ("Hello, world!"));
+    xclose (fd);
+  }
+
+  printf ("info: Write to %s\n", file_path);
+  {
+    int fd = xopen (file_path, O_WRONLY | O_TRUNC, 0);
+    xwrite (fd, "Good day to you too.", strlen ("Good day to you too."));
+    xclose (fd);
+  }
+
+  printf ("info: Attempt O_EXCL creation of existing %s\n", file_path);
+  /* O_EXCL creation shall fail.  */
+  errno = 0;
+  TEST_COMPARE (open64 (file_path, O_RDWR | O_EXCL | O_CREAT, 0600), -1);
+  TEST_COMPARE (errno, EEXIST);
+
+  free (file_path);
+
+  {
+    char *new_path = xasprintf ("%s/new", support_fuse_mountpoint (f));
+    printf ("info: Test successful O_EXCL creation at %s\n", new_path);
+    int fd = xopen (new_path, O_RDWR | O_EXCL | O_CREAT, 0600);
+    xclose (fd);
+    free (new_path);
+  }
+
+  {
+    char *subdir_path = xasprintf ("%s/subdir", support_fuse_mountpoint (f));
+    xmkdir (subdir_path, 01234);
+  }
+
+  {
+    char *symlink_path = xasprintf ("%s/symlink", support_fuse_mountpoint (f));
+    char *target = xreadlink (symlink_path);
+    TEST_COMPARE_STRING (target, "target-of-symbolic-link");
+    free (target);
+    free (symlink_path);
+  }
+
+  support_fuse_unmount (f);
+  return 0;
+}
+
+#include <support/test-driver.c>