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