From f169509ded534537eec9df00cfada6dbca908352 Mon Sep 17 00:00:00 2001 From: Florian Weimer Date: Fri, 30 Aug 2024 21:52:53 +0200 Subject: [PATCH] support: Add FUSE-based file system test framework to support/ This allows to monitor the exact file system operations performed by glibc and inject errors. Hurd does not have . To get the sources to compile at least, the same approach as in support/test-container.c is used. Reviewed-by: DJ Delorie --- 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 8fb4d2c500..93d32ae75f 100644 --- a/support/Makefile +++ b/support/Makefile @@ -64,6 +64,7 @@ libsupport-routines = \ support_format_herrno \ support_format_hostent \ support_format_netent \ + support_fuse \ support_isolate_in_subprocess \ support_mutex_pi_monotonic \ support_need_proc \ @@ -327,6 +328,7 @@ tests = \ tst-support_capture_subprocess \ tst-support_descriptors \ tst-support_format_dns_packet \ + tst-support_fuse \ tst-support_quote_blob \ tst-support_quote_blob_wide \ tst-support_quote_string \ diff --git a/support/fuse.h b/support/fuse.h new file mode 100644 index 0000000000..4c365fbc0c --- /dev/null +++ b/support/fuse.h @@ -0,0 +1,215 @@ +/* Facilities for FUSE-backed file system tests. + Copyright (C) 2024 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +/* 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 +#include +#include +#include + +#include + +/* 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 + . */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +# include +#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 + . */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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