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 <sys/mount.h>.  To get the sources to compile
at least, the same approach as in support/test-container.c is used.

Reviewed-by: DJ Delorie <dj@redhat.com>
This commit is contained in:
Florian Weimer 2024-08-30 21:52:53 +02:00
parent 61f2c2e1d1
commit f169509ded
4 changed files with 1270 additions and 0 deletions

View File

@ -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 \

215
support/fuse.h Normal file
View File

@ -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 */

705
support/support_fuse.c Normal file
View File

@ -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;
}

348
support/tst-support_fuse.c Normal file
View File

@ -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>