diff --git a/misc/Makefile b/misc/Makefile index 7b7f8351bf..1422c95317 100644 --- a/misc/Makefile +++ b/misc/Makefile @@ -292,6 +292,12 @@ tests-static := tst-empty tests-internal += tst-fd_to_filename tests-static += tst-fd_to_filename +# Tests with long run times. +xtests += \ + tst-mkstemp-fuse \ + tst-mkstemp-fuse-parallel \ + # xtests + ifeq ($(run-built-tests),yes) ifeq (yes,$(build-shared)) ifneq ($(PERL),no) diff --git a/misc/tst-mkstemp-fuse-parallel.c b/misc/tst-mkstemp-fuse-parallel.c new file mode 100644 index 0000000000..219f26cb3b --- /dev/null +++ b/misc/tst-mkstemp-fuse-parallel.c @@ -0,0 +1,219 @@ +/* FUSE-based test for mkstemp. Parallel collision statistics. + 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 + +/* The number of subprocesses that call mkstemp. */ +static pid_t processes[4]; + +/* Enough space to record the expected number of replies (62**3) for + each process. */ +enum { results_allocated = array_length (processes) * 62 * 62 * 62 }; + +/* The thread will store the results there. */ +static uint64_t *results; + +/* Currently used part of the results array. */ +static size_t results_used; + +/* Fail with EEXIST (so that mkstemp tries again). Record observed + names for later statistical analysis. */ +static void +fuse_thread (struct support_fuse *f, void *closure) +{ + struct fuse_in_header *inh; + while ((inh = support_fuse_next (f)) != NULL) + { + if (support_fuse_handle_mountpoint (f) + || (inh->nodeid == 1 && support_fuse_handle_directory (f))) + continue; + if (inh->opcode != FUSE_LOOKUP || results_used >= results_allocated) + { + support_fuse_reply_error (f, EIO); + continue; + } + + char *name = support_fuse_cast (LOOKUP, inh); + TEST_COMPARE_BLOB (name, 3, "new", 3); + TEST_COMPARE (strlen (name), 9); + /* Extract 8 bytes of the name: 'w', the X replacements, and the + null terminator. Treat it as an uint64_t for easy sorting + below. Endianess does not matter because the relative order + of the entries is not important; sorting is only used to find + duplicates. */ + TEST_VERIFY_EXIT (results_used < results_allocated); + memcpy (&results[results_used], name + 2, 8); + ++results_used; + struct fuse_entry_out *out = support_fuse_prepare_entry (f, 2); + out->attr.mode = S_IFREG | 0600; + support_fuse_reply_prepared (f); + } +} + +/* Used to sort the results array, to find duplicates. */ +static int +results_sort (const void *a1, const void *b1) +{ + const uint64_t *a = a1; + const uint64_t *b = b1; + if (*a < *b) + return -1; + if (*a == *b) + return 0; + return 1; +} + +/* Number of occurrences of certain streak lengths. */ +static size_t streak_lengths[6]; + +/* Called for every encountered streak. */ +static inline void +report_streak (uint64_t current, size_t length) +{ + if (length > 1) + { + printf ("info: name \"ne%.8s\" repeats: %zu\n", + (char *) ¤t, length); + TEST_VERIFY_EXIT (length < array_length (streak_lengths)); + } + TEST_VERIFY_EXIT (length < array_length (streak_lengths)); + ++streak_lengths[length]; +} + +static int +do_test (void) +{ + support_fuse_init (); + + results = xmalloc (results_allocated * sizeof (*results)); + + struct shared + { + /* Used to synchronize the start of all subprocesses, to make it + more likely to expose concurrency-related bugs. */ + pthread_barrier_t barrier1; + pthread_barrier_t barrier2; + + /* Filled in after fork. */ + char mountpoint[4096]; + }; + + /* Used to synchronize the start of all subprocesses, to make it + more likely to expose concurrency-related bugs. */ + struct shared *pshared = support_shared_allocate (sizeof (*pshared)); + { + pthread_barrierattr_t attr; + xpthread_barrierattr_init (&attr); + xpthread_barrierattr_setpshared (&attr, PTHREAD_PROCESS_SHARED); + xpthread_barrierattr_destroy (&attr); + xpthread_barrier_init (&pshared->barrier1, &attr, + array_length (processes) + 1); + xpthread_barrier_init (&pshared->barrier2, &attr, + array_length (processes) + 1); + xpthread_barrierattr_destroy (&attr); + } + + for (int i = 0; i < array_length (processes); ++i) + { + processes[i] = xfork (); + if (processes[i] == 0) + { + /* Wait for mountpoint initialization. */ + xpthread_barrier_wait (&pshared->barrier1); + char *path = xasprintf ("%s/newXXXXXX", pshared->mountpoint); + + /* Park this process until all processes have started. */ + xpthread_barrier_wait (&pshared->barrier2); + errno = 0; + TEST_COMPARE (mkstemp (path), -1); + TEST_COMPARE (errno, EEXIST); + free (path); + _exit (0); + } + } + + /* Do this after the forking, to minimize initialization inteference. */ + struct support_fuse *f = support_fuse_mount (fuse_thread, NULL); + TEST_VERIFY (strlcpy (pshared->mountpoint, support_fuse_mountpoint (f), + sizeof (pshared->mountpoint)) + < sizeof (pshared->mountpoint)); + xpthread_barrier_wait (&pshared->barrier1); + + puts ("info: performing mkstemp calls"); + xpthread_barrier_wait (&pshared->barrier2); + + for (int i = 0; i < array_length (processes); ++i) + { + int status; + xwaitpid (processes[i], &status, 0); + TEST_COMPARE (status, 0); + } + + support_fuse_unmount (f); + xpthread_barrier_destroy (&pshared->barrier2); + xpthread_barrier_destroy (&pshared->barrier1); + + printf ("info: checking results (count %zu)\n", results_used); + qsort (results, results_used, sizeof (*results), results_sort); + + uint64_t current = -1; + size_t streak = 0; + for (size_t i = 0; i < results_used; ++i) + if (results[i] == current) + ++streak; + else + { + report_streak (current, streak); + current = results[i]; + streak = 1; + } + report_streak (current, streak); + + puts ("info: repetition count distribution:"); + for (int i = 1; i < array_length (streak_lengths); ++i) + printf (" length %d: %zu\n", i, streak_lengths[i]); + /* Some arbitrary threshold, hopefully unlikely enough. In over + 260,000 runs of a simulation of this test, at most 26 pairs were + observed, and only one three-way collisions. */ + if (streak_lengths[2] > 30) + FAIL ("unexpected repetition count 2: %zu", streak_lengths[2]); + if (streak_lengths[3] > 2) + FAIL ("unexpected repetition count 3: %zu", streak_lengths[3]); + for (int i = 4; i < array_length (streak_lengths); ++i) + if (streak_lengths[i] > 0) + FAIL ("too many repeats of count %d: %zu", i, streak_lengths[i]); + + free (results); + + return 0; +} + +#include diff --git a/misc/tst-mkstemp-fuse.c b/misc/tst-mkstemp-fuse.c new file mode 100644 index 0000000000..5ac6a6872a --- /dev/null +++ b/misc/tst-mkstemp-fuse.c @@ -0,0 +1,197 @@ +/* FUSE-based test for mkstemp. + 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 + +/* Set to true in do_test to cause the first FUSE_CREATE attempt to fail. */ +static _Atomic bool simulate_creat_race; + +/* Basic tests with eventually successful creation. */ +static void +fuse_thread_basic (struct support_fuse *f, void *closure) +{ + char *previous_name = NULL; + int state = 0; + struct fuse_in_header *inh; + while ((inh = support_fuse_next (f)) != NULL) + { + if (support_fuse_handle_mountpoint (f) + || (inh->nodeid == 1 && support_fuse_handle_directory (f))) + continue; + + switch (inh->opcode) + { + case FUSE_LOOKUP: + /* File does not exist initially. */ + TEST_COMPARE (inh->nodeid, 1); + if (simulate_creat_race) + { + if (state < 3) + ++state; + else + FAIL ("invalid state: %d", state); + } + else + { + TEST_COMPARE (state, 0); + state = 3; + } + support_fuse_reply_error (f, ENOENT); + break; + case FUSE_CREATE: + { + TEST_COMPARE (inh->nodeid, 1); + char *name; + struct fuse_create_in *p + = support_fuse_cast_name (CREATE, inh, &name); + /* Name follows after struct fuse_create_in. */ + TEST_COMPARE (p->flags & O_ACCMODE, O_RDWR); + TEST_VERIFY (p->flags & O_EXCL); + TEST_VERIFY (p->flags & O_CREAT); + TEST_COMPARE (p->mode & 07777, 0600); + TEST_VERIFY (S_ISREG (p->mode)); + TEST_COMPARE_BLOB (name, 3, "new", 3); + + if (state != 3 && simulate_creat_race) + { + ++state; + support_fuse_reply_error (f, EEXIST); + } + else + { + if (previous_name != NULL) + /* This test has a very small probability of failure + due to a harmless collision (one in 62**6 tests). */ + TEST_VERIFY (strcmp (name, previous_name) != 0); + TEST_COMPARE (state, 3); + ++state; + struct fuse_entry_out *entry; + struct fuse_open_out *open; + support_fuse_prepare_create (f, 2, &entry, &open); + entry->attr.mode = S_IFREG | 0600; + support_fuse_reply_prepared (f); + } + free (previous_name); + previous_name = xstrdup (name); + } + break; + case FUSE_FLUSH: + case FUSE_RELEASE: + TEST_COMPARE (state, 4); + TEST_COMPARE (inh->nodeid, 2); + support_fuse_reply_empty (f); + break; + default: + support_fuse_reply_error (f, EIO); + } + } + free (previous_name); +} + +/* Reply that all files exist. */ +static void +fuse_thread_eexist (struct support_fuse *f, void *closure) +{ + uint64_t counter = 0; + struct fuse_in_header *inh; + while ((inh = support_fuse_next (f)) != NULL) + { + if (support_fuse_handle_mountpoint (f) + || (inh->nodeid == 1 && support_fuse_handle_directory (f))) + continue; + + switch (inh->opcode) + { + case FUSE_LOOKUP: + ++counter; + TEST_COMPARE (inh->nodeid, 1); + char *name = support_fuse_cast (LOOKUP, inh); + TEST_COMPARE_BLOB (name, 3, "new", 3); + TEST_COMPARE (strlen (name), 9); + for (int i = 3; i <= 8; ++i) + { + /* The glibc implementation uses letters and digits only. */ + char ch = name[i]; + TEST_VERIFY (('0' <= ch && ch <= '9') + || ('a' <= ch && ch <= 'z') + || ('A' <= ch && ch <= 'Z')); + } + struct fuse_entry_out out = + { + .nodeid = 2, + .attr = { + .mode = S_IFREG | 0600, + .ino = 2, + }, + }; + support_fuse_reply (f, &out, sizeof (out)); + break; + default: + support_fuse_reply_error (f, EIO); + } + } + /* Verify that mkstemp has retried a lot. The current + implementation tries 62 * 62 * 62 times until it goves up. */ + TEST_VERIFY (counter >= 200000); +} + +static int +do_test (void) +{ + support_fuse_init (); + + for (int do_simulate_creat_race = 0; do_simulate_creat_race < 2; + ++do_simulate_creat_race) + { + simulate_creat_race = do_simulate_creat_race; + printf ("info: testing with simulate_creat_race == %d\n", + (int) simulate_creat_race); + struct support_fuse *f = support_fuse_mount (fuse_thread_basic, NULL); + char *path = xasprintf ("%s/newXXXXXX", support_fuse_mountpoint (f)); + int fd = mkstemp (path); + TEST_VERIFY (fd > 2); + xclose (fd); + free (path); + support_fuse_unmount (f); + } + + puts ("info: testing EEXIST failure case for mkstemp"); + { + struct support_fuse *f = support_fuse_mount (fuse_thread_eexist, NULL); + char *path = xasprintf ("%s/newXXXXXX", support_fuse_mountpoint (f)); + errno = 0; + TEST_COMPARE (mkstemp (path), -1); + TEST_COMPARE (errno, EEXIST); + free (path); + support_fuse_unmount (f); + } + + return 0; +} + +#include