[v4,05/13] support: Add FUSE-based file system test framework to support/
Checks
Context |
Check |
Description |
redhat-pt-bot/TryBot-apply_patch |
success
|
Patch applied to master at the time it was sent
|
linaro-tcwg-bot/tcwg_glibc_build--master-arm |
success
|
Build passed
|
linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 |
success
|
Build passed
|
linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 |
success
|
Test passed
|
linaro-tcwg-bot/tcwg_glibc_check--master-arm |
success
|
Test passed
|
Commit Message
This allows to monitor the exact file system operations
performed by glibc and inject errors.
Hurd does not have <sys/mount.h>. To get the sources to compile
at least, the same approach as in support/test-container.c is used.
---
support/Makefile | 2 +
support/fuse.h | 215 +++++++++++
support/support_fuse.c | 705 +++++++++++++++++++++++++++++++++++++
support/tst-support_fuse.c | 348 ++++++++++++++++++
4 files changed, 1270 insertions(+)
create mode 100644 support/fuse.h
create mode 100644 support/support_fuse.c
create mode 100644 support/tst-support_fuse.c
Comments
Florian Weimer <fweimer@redhat.com> writes:
> This allows to monitor the exact file system operations
> performed by glibc and inject errors.
>
> Hurd does not have <sys/mount.h>. To get the sources to compile
> at least, the same approach as in support/test-container.c is used.
LGTM
Reviewed-by: DJ Delorie <dj@redhat.com>
> diff --git a/support/Makefile b/support/Makefile
> + support_fuse \
> + tst-support_fuse \
Ok.
> diff --git a/support/fuse.h b/support/fuse.h
> new file mode 100644
> +/* 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>
Ok.
> +/* This function must be called furst, before support_fuse_mount, to
> + prepare unprivileged mounting. */
> +void support_fuse_init (void);
Ok.
> +/* 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);
Ok.
> +/* Opaque type for tracking FUSE mount state. */
> +struct support_fuse;
Ok.
> +/* This function disables a mount point created using
> + support_fuse_mount. */
> +void support_fuse_unmount (struct support_fuse *) __nonnull ((1));
Ok.
> +/* 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);
Ok.
> +/* 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);
Ok.
> +/* 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));
Ok.
> +
> +/* 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;
Ok.
> +/* 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));
Ok.
> +/* 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))
Ok.
> +/* 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)))
Ok.
> +/* 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));
Ok.
> +/* 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));
Ok.
> +/* 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));
Ok.
> +/* 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));
Ok.
> +/* 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));
Ok.
> +/* 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));
Ok.
> +/* 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));
Ok.
> +/* 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));
Ok.
> +/* 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));
Ok.
> +
> +/* 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);
Ok.
> +/* 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);
Ok.
> +/* 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 */
Ok.
> diff --git a/support/support_fuse.c b/support/support_fuse.c
> +/* 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>
Ok.
> +#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
Ok.
> +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;
> +};
Ok.
> +struct fuse_thread_wrapper_args
> +{
> + struct support_fuse *f;
> + support_fuse_callback callback;
> + void *closure;
> +};
Ok.
> +/* Set by support_fuse_init to indicate that support_fuse_mount may be
> + called. */
> +static bool support_fuse_init_called;
Ok.
> +/* 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));
Ok.
> +/* Internal mkdtemp replacement */
> +static char * support_fuse_mkdir (const char *prefix) __nonnull ((1));
Ok.
> +/* Low-level allocation function for support_fuse_mount. Does not
> + perform the mount. */
> +static struct support_fuse *support_fuse_open (void);
Ok.
> +/* Thread wrapper function for use with pthread_create. Uses struct
> + fuse_thread_wrapper_args. */
> +static void *support_fuse_thread_wrapper (void *closure) __nonnull ((1));
Ok.
> +/* 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);
Ok.
> +/* 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));
Ok.
> +/* 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;
Ok.
> +/* PID of the process that should clean up the mount points in the ELF
> + destructor. */
> +static pid_t support_fuse_cleanup_pid;
Ok.
> +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;
> +}
Ok.
> +void
> +support_fuse_filter_forget (struct support_fuse *f, bool filter)
> +{
> + f->filter_forget = filter;
> +}
Ok.
> +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;
> +}
Ok.
> +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;
> +}
Ok.
> +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;
> +}
Ok.
> +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;
> + }
> +}
Ok.
> +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;
> +}
Ok.
> +void
> +support_fuse_init (void)
> +{
> + support_fuse_init_called = true;
> +
> + support_become_root ();
> + if (!support_enter_mount_namespace ())
> + FAIL_UNSUPPORTED ("mount namespaces not supported");
> +}
Ok.
> +void
> +support_fuse_init_no_namespace (void)
> +{
> + support_fuse_init_called = true;
> +}
Ok.
> +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;
> + }
> +}
Ok.
> +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;
> + }
> +}
Ok.
> +const char *
> +support_fuse_mountpoint (struct support_fuse *f)
> +{
> + return f->mountpoint;
> +}
Ok.
> +void
> +support_fuse_no_reply (struct support_fuse *f)
> +{
> + TEST_VERIFY (f->inh != NULL);
> + TEST_COMPARE (f->inh->opcode, FUSE_FORGET);
> + f->inh = NULL;
> +}
Ok.
> +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);
> +}
Ok.
> +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;
> +}
Ok.
> +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;
> +}
Ok.
> +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;
> +}
Ok.
> +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;
> +}
Ok.
> +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;
> +}
Ok.
> +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;
> +}
Ok.
> +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;
> + }
Buffer has packet, use it, ok.
> + 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");
Get one or more new packets, ok.
> + 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;
> + }
Ok.
> + if (ret < 0)
> + {
> + if (f->disconnected)
> + /* Unmount detected. */
> + return NULL;
> + FAIL_EXIT1 ("read error on FUSE device: %m");
> + }
Ok.
> + /* Read was successful, make [next, limit) the active buffer area. */
> + f->buffer_next = f->buffer_start;
> + f->buffer_limit = (void *) f->buffer_start + ret;
> + }
> +}
Buffer now has packet, go back and use it.
> +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;
> +}
Ok.
> +void
> +support_fuse_reply_empty (struct support_fuse *f)
> +{
> + support_fuse_reply_error_1 (f, 0);
> +}
Ok.
> +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;
> +}
Ok.
> +void
> +support_fuse_reply_error (struct support_fuse *f, uint32_t error)
> +{
> + TEST_VERIFY (error > 0);
> + support_fuse_reply_error_1 (f, error);
> +}
Ok.
> +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);
> +}
Ok.
> +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;
> +}
Ok.
> +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);
> +}
Ok.
> +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 ();
> +}
Ok.
> +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;
> +}
Ok.
> diff --git a/support/tst-support_fuse.c b/support/tst-support_fuse.c
> +/* 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>
Ok.
> +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);
> + }
Ok.
> + /* 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;
Ok.
> + 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;
Ok.
> + 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;
Ok.
> + 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;
Ok.
> + 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;
Ok.
> + 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;
Ok.
> + case FUSE_FLUSH:
> + /* Sent in response to close. */
> + support_fuse_reply_empty (f);
> + break;
Ok.
> + 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;
Ok.
> + 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;
Ok.
> + 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;
Ok.
> + 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;
Ok.
> + 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;
Ok.
> + 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;
Ok.
> + case FUSE_FORGET:
> + support_fuse_no_reply (f);
> + break;
> + default:
> + support_fuse_reply_error (f, EIO);
> + }
> + }
> +}
Ok.
> +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);
> + }
Ok.
> + 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);
> + }
Ok.
> + 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!"));
> + }
Ok.
> + 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);
> + }
Ok.
> + 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);
> + }
Ok.
> + 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);
Ok.
> + {
> + 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);
> + }
Ok.
> + {
> + char *subdir_path = xasprintf ("%s/subdir", support_fuse_mountpoint (f));
> + xmkdir (subdir_path, 01234);
> + }
Ok.
> + {
> + 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);
> + }
Ok.
> + support_fuse_unmount (f);
> + return 0;
> +}
> +
> +#include <support/test-driver.c>
Ok.
On 30/08/24 16:52, Florian Weimer wrote:
> This allows to monitor the exact file system operations
> performed by glibc and inject errors.
>
> Hurd does not have <sys/mount.h>. To get the sources to compile
> at least, the same approach as in support/test-container.c is used.
> ---
> support/Makefile | 2 +
> support/fuse.h | 215 +++++++++++
> support/support_fuse.c | 705 +++++++++++++++++++++++++++++++++++++
> support/tst-support_fuse.c | 348 ++++++++++++++++++
> 4 files changed, 1270 insertions(+)
> create mode 100644 support/fuse.h
> create mode 100644 support/support_fuse.c
> create mode 100644 support/tst-support_fuse.c
>
> diff --git a/support/Makefile b/support/Makefile
> index ec9793ab1e..fe9a099bed 100644
> --- a/support/Makefile
> +++ b/support/Makefile
> @@ -62,6 +62,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 \
> @@ -324,6 +325,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
s/furst/first.
> + 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));
I think we undefined it for glibc itself (include/sys/cdefs.h:16), does
it work for libsupport?
> +
> +/* 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
s/reponse/response
> + 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>
* Adhemerval Zanella Netto:
> On 30/08/24 16:52, Florian Weimer wrote:
>> +#include <support/bundled/linux/include/uapi/linux/fuse.h>
>> +
>> +/* This function must be called furst, before support_fuse_mount, to
>
> s/furst/first.
I'll push a fix.
>> +/* This function disables a mount point created using
>> + support_fuse_mount. */
>> +void support_fuse_unmount (struct support_fuse *) __nonnull ((1));
>
> I think we undefined it for glibc itself (include/sys/cdefs.h:16), does
> it work for libsupport?
Yes, _ISOMAC is not defined while building libsupport.
>> +/* Specific reponse preparation functions. The returned object can be
>
> s/reponse/response
Will push a fix for that, too.
Thanks,
Florian
@@ -62,6 +62,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 \
@@ -324,6 +325,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 \
new file mode 100644
@@ -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 */
new file mode 100644
@@ -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;
+}
new file mode 100644
@@ -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>