blob: 0ef8c67c126b35f4da6c3f19c822f10c1ea6c102 [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0+
/*
* stress test for EROFS filesystem
*
* Copyright (C) 2019-2025 Gao Xiang <xiang@kernel.org>
*/
#define _FILE_OFFSET_BITS 64
#define _GNU_SOURCE
#include "erofs/defs.h"
#include <errno.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#define MAX_CHUNKSIZE (2 * 1024 * 1024)
#define MAX_SCAN_CHUNKSIZE (256 * 1024)
bool superuser;
unsigned int nprocs = 1, loops = 1, r_seed;
unsigned int procid;
volatile sig_atomic_t should_stop;
enum {
DROP_PAGE_CACHE,
DROP_SLAB_CACHE,
COMPACT_MEMORY,
};
enum {
OP_GETDENTS,
OP_READLINK,
OP_SEQREAD_ALIGNED,
OP_SEQREAD_UNALIGNED,
OP_READ,
OP_FADVISE,
OP_DROP_CACHES,
};
struct opdesc {
char *name;
int (*func)(int op, unsigned int sn);
int freq;
bool requireroot;
};
extern struct opdesc ops[];
static int drop_caches_f(int op, unsigned int sn)
{
static const char *procfile[] = {
[DROP_PAGE_CACHE] = "/proc/sys/vm/drop_caches",
[DROP_SLAB_CACHE] = "/proc/sys/vm/drop_caches",
[COMPACT_MEMORY] = "/proc/sys/vm/compact_memory",
};
static const char *val[] = {
[DROP_PAGE_CACHE] = "1\n",
[DROP_SLAB_CACHE] = "2\n",
[COMPACT_MEMORY] = "1\n",
};
int mode = random() % ARRAY_SIZE(val);
FILE *f;
clock_t start;
if (!procfile[mode])
return -EINVAL;
printf("%d[%u]/%u %s: %s=%s", getpid(), procid, sn, __func__,
procfile[mode], val[mode]);
f = fopen(procfile[mode], "w");
if (!f)
return -errno;
start = clock();
while (clock() < start + CLOCKS_PER_SEC) {
fputs(val[mode], f);
(void)sched_yield();
}
fclose(f);
return 0;
}
struct fent {
char *subpath;
int fd, chkfd;
};
#define FT_DIR 0
#define FT_DIRm (1 << FT_DIR)
#define FT_REG 1
#define FT_REGm (1 << FT_REG)
#define FT_SYM 2
#define FT_SYMm (1 << FT_SYM)
#define FT_DEV 3
#define FT_DEVm (1 << FT_DEV)
#define FT_nft 4
#define FT_ANYm ((1 << FT_nft) - 1)
#define FLIST_SLOT_INCR 16
struct flist {
int nfiles, nslots;
struct fent *fents;
} flists[FT_nft];
static struct fent *add_to_flist(int type, char *subpath)
{
struct fent *fep;
struct flist *ftp;
ftp = &flists[type];
if (ftp->nfiles >= ftp->nslots) {
ftp->nslots += FLIST_SLOT_INCR;
ftp->fents = realloc(ftp->fents,
ftp->nslots * sizeof(struct fent));
if (!ftp->fents)
return NULL;
}
fep = &ftp->fents[ftp->nfiles++];
fep->subpath = strdup(subpath);
fep->fd = -1;
fep->chkfd = -1;
return fep;
}
static inline bool is_dot_dotdot(const char *name)
{
if (name[0] != '.')
return false;
return name[1] == '\0' || (name[1] == '.' && name[2] == '\0');
}
static int walkdir(struct fent *ent)
{
const char *dirpath = ent->subpath;
int ret = 0;
struct dirent *dp;
DIR *_dir;
_dir = opendir(dirpath);
if (!_dir) {
fprintf(stderr, "failed to opendir at %s: %s\n",
dirpath, strerror(errno));
return -errno;
}
while (1) {
char subpath[PATH_MAX];
struct stat st;
/*
* set errno to 0 before calling readdir() in order to
* distinguish end of stream and from an error.
*/
errno = 0;
dp = readdir(_dir);
if (!dp)
break;
if (is_dot_dotdot(dp->d_name))
continue;
sprintf(subpath, "%s/%s", dirpath, dp->d_name);
if (lstat(subpath, &st))
continue;
switch (st.st_mode & S_IFMT) {
case S_IFDIR:
ent = add_to_flist(FT_DIR, subpath);
if (ent == NULL) {
ret = -ENOMEM;
goto err_closedir;
}
ret = walkdir(ent);
if (ret)
goto err_closedir;
break;
case S_IFREG:
ent = add_to_flist(FT_REG, subpath);
if (ent == NULL) {
ret = -ENOMEM;
goto err_closedir;
}
break;
case S_IFLNK:
ent = add_to_flist(FT_SYM, subpath);
if (ent == NULL) {
ret = -ENOMEM;
goto err_closedir;
}
break;
default:
break;
}
}
if (errno)
ret = -errno;
err_closedir:
closedir(_dir);
return ret;
}
static int init_filetable(int testdir_fd)
{
struct fent *fent;
fent = add_to_flist(FT_DIR, ".");
if (!fent)
return -ENOMEM;
if (fchdir(testdir_fd) < 0) {
perror("failed to fchdir");
return -errno;
}
return walkdir(fent);
}
static struct fent *getfent(int which, int r)
{
int totalsum = 0; /* total number of matching files */
int partialsum = 0; /* partial sum of matching files */
struct flist *flp;
int i, x;
totalsum = 0;
for (i = 0, flp = flists; i < FT_nft; ++i, ++flp)
if (which & (1 << i))
totalsum += flp->nfiles;
if (!totalsum)
return NULL;
/*
* Now we have possible matches between 0..totalsum-1.
* And we use r to help us choose which one we want,
* which when bounded by totalsum becomes x.
*/
x = (int)(r % totalsum);
for (i = 0, flp = flists; i < FT_nft; i++, flp++) {
if (which & (1 << i)) {
if (x < partialsum + flp->nfiles)
return &flp->fents[x - partialsum];
partialsum += flp->nfiles;
}
}
fprintf(stderr, "%s failure\n", __func__);
return NULL;
}
static int testdir_fd = -1, chkdir_fd = -1;
static char *dumpfile;
static int __getdents_f(unsigned int sn, struct fent *fe)
{
int dfd;
DIR *dir;
dfd = openat(testdir_fd, fe->subpath, O_DIRECTORY);
if (dfd < 0) {
fprintf(stderr, "%d[%u]/%u getdents_f: failed to open directory %s",
getpid(), procid, sn, fe->subpath);
return -errno;
}
dir = fdopendir(dfd);
while (readdir(dir) != NULL)
continue;
closedir(dir);
return 0;
}
static int getdents_f(int op, unsigned int sn)
{
struct fent *fe;
fe = getfent(FT_DIRm, random());
if (!fe)
return 0;
printf("%d[%u]/%u %s: %s\n", getpid(), procid, sn, __func__,
fe->subpath);
return __getdents_f(sn, fe);
}
static void baddump(unsigned int sn, const char *op,
char *buf1, char *buf2, unsigned int sz)
{
int fd, err, i;
char *fn = dumpfile;
if (!fn)
return;
for (i = 0;;) {
fd = open(fn, O_CREAT | O_EXCL | O_WRONLY, 0644);
if (fd >= 0) {
printf("%d[%u]/%u %s: dump inconsistent data to \"%s\" of %u bytes\n",
getpid(), procid, sn, op, fn, sz);
if (fn != dumpfile)
free(fn);
break;
}
if (fd < 0 && errno != EEXIST) {
fprintf(stderr, "%d[%u]/%u: failed to create dumpfile %s\n",
getpid(), procid, sn, fn);
if (fn != dumpfile)
free(fn);
return;
}
if (fn != dumpfile)
free(fn);
err = asprintf(&fn, "%s.%d", dumpfile, ++i);
if (err < 0) {
fprintf(stderr, "%d[%u]/%u: failed to allocate filename\n",
getpid(), procid, sn);
return;
}
}
if (write(fd, buf1, sz) != sz)
fprintf(stderr, "%d[%u]/%u: failed to write buffer1 @ %u\n",
getpid(), procid, sn, sz);
if (write(fd, buf2, sz) != sz)
fprintf(stderr, "%d[%u]/%u: failed to write buffer2 @ %u\n",
getpid(), procid, sn, sz);
close(fd);
}
static int readlink_f(int op, unsigned int sn)
{
char buf1[PATH_MAX], buf2[PATH_MAX];
struct fent *fe;
ssize_t sz;
fe = getfent(FT_SYMm, random());
if (!fe)
return 0;
printf("%d[%u]/%u %s: %s\n", getpid(), procid, sn, __func__,
fe->subpath);
sz = readlinkat(testdir_fd, fe->subpath, buf1, PATH_MAX - 1);
if (sz < 0) {
fprintf(stderr, "%d[%u]/%u %s: failed to readlinkat %s: %d",
getpid(), procid, sn, __func__, fe->subpath, errno);
return -errno;
}
if (chkdir_fd >= 0) {
if (sz != readlinkat(testdir_fd, fe->subpath, buf2,
PATH_MAX - 1)) {
fprintf(stderr, "%d[%u]/%u %s: symlink length mismatch @%s\n",
getpid(), procid, sn, __func__, fe->subpath);
return -E2BIG;
}
if (memcmp(buf1, buf2, sz)) {
fprintf(stderr, "%d[%u]/%u %s: symlink mismatch @%s\n",
getpid(), procid, sn, __func__, fe->subpath);
baddump(sn, "readlink_f", buf1, buf2, sz);
return -EBADMSG;
}
}
return 0;
}
static int tryopen(unsigned int sn, const char *op, struct fent *fe)
{
if (fe->fd < 0) {
fe->fd = openat(testdir_fd, fe->subpath, O_RDONLY);
if (fe->fd < 0) {
fprintf(stderr, "%d[%u]/%u %s: failed to open %s: %d",
getpid(), procid, sn, op, fe->subpath, errno);
return -errno;
}
/* use force_page_cache_readahead for every read request */
posix_fadvise(fe->fd, 0, 0, POSIX_FADV_RANDOM);
}
if (chkdir_fd >= 0 && fe->chkfd < 0)
fe->chkfd = openat(chkdir_fd, fe->subpath, O_RDONLY);
return 0;
}
static int fadvise_f(int op, unsigned int sn)
{
struct fent *fe;
int ret;
fe = getfent(FT_REGm, random());
if (!fe)
return 0;
ret = tryopen(sn, __func__, fe);
if (ret)
return ret;
printf("%d[%u]/%u %s: %s\n", getpid(), procid, sn,
__func__, fe->subpath);
ret = posix_fadvise(fe->fd, 0, 0, POSIX_FADV_DONTNEED);
if (!ret)
return 0;
fprintf(stderr, "%d(%u)/%u %s: posix_fadvise %s failed %d\n",
getpid(), procid, sn, __func__, fe->subpath, errno);
return -errno;
}
static int __read_f(unsigned int sn, struct fent *fe, uint64_t filesize)
{
static char buf[MAX_CHUNKSIZE], chkbuf[MAX_CHUNKSIZE];
uint64_t lr, off, len, trimmed;
size_t nread, nread2;
lr = ((uint64_t) random() << 32) + random();
off = lr % filesize;
len = (random() % MAX_CHUNKSIZE) + 1;
trimmed = len;
if (off + len > filesize) {
uint64_t a = filesize - off + 16 * getpagesize();
if (len > a)
len %= a;
trimmed = len <= filesize - off ? len : filesize - off;
}
printf("%d[%u]/%u read_f: %llu bytes @ %llu of %s\n", getpid(), procid,
sn, len | 0ULL, off | 0ULL, fe->subpath);
nread = pread(fe->fd, buf, len, off);
if (nread != trimmed) {
fprintf(stderr, "%d[%u]/%u read_f: failed to read %llu bytes @ %llu of %s\n",
getpid(), procid, sn, len | 0ULL, off | 0ULL,
fe->subpath);
return -errno;
}
if (fe->chkfd < 0)
return 0;
nread2 = pread(fe->chkfd, chkbuf, len, off);
if (nread2 <= 0) {
fprintf(stderr, "%d[%u]/%u read_f: failed to check %llu bytes @ %llu of %s\n",
getpid(), procid, sn, len | 0ULL, off | 0ULL,
fe->subpath);
return -errno;
}
if (nread != nread2) {
fprintf(stderr, "%d[%u]/%u read_f: size mismatch %llu bytes @ %llu of %s\n",
getpid(), procid, sn, len | 0ULL, off | 0ULL,
fe->subpath);
return -EFBIG;
}
if (memcmp(buf, chkbuf, nread)) {
fprintf(stderr, "%d[%u]/%u read_f: data mismatch %llu bytes @ %llu of %s\n",
getpid(), procid, sn, len | 0ULL, off | 0ULL,
fe->subpath);
baddump(sn, "read_f", buf, chkbuf, nread);
return -EBADMSG;
}
return 0;
}
static int read_f(int op, unsigned int sn)
{
struct fent *fe;
ssize_t fsz;
int ret;
fe = getfent(FT_REGm, random());
if (!fe)
return 0;
ret = tryopen(sn, __func__, fe);
if (ret)
return ret;
fsz = lseek(fe->fd, 0, SEEK_END);
if (fsz <= 0) {
if (!fsz) {
printf("%d[%u]/%u %s: zero size @ %s\n",
getpid(), procid, sn, __func__, fe->subpath);
return 0;
}
fprintf(stderr, "%d[%u]/%u %s: lseek %s failed %d\n",
getpid(), procid, sn, __func__, fe->subpath, errno);
return -errno;
}
return __read_f(sn, fe, fsz);
}
static int __doscan_f(unsigned int sn, const char *op, struct fent *fe,
uint64_t filesize, uint64_t chunksize)
{
static char buf[MAX_SCAN_CHUNKSIZE], chkbuf[MAX_SCAN_CHUNKSIZE];
uint64_t pos;
printf("%d[%u]/%u %s: filesize %llu, chunksize %llu @ %s\n",
getpid(), procid, sn, op, (unsigned long long)filesize,
(unsigned long long)chunksize, fe->subpath);
for (pos = 0; pos < filesize; pos += chunksize) {
ssize_t nread, nread2;
nread = pread(fe->fd, buf, chunksize, pos);
if (nread <= 0)
return -errno;
if (nread < chunksize && nread != filesize - pos)
return -ERANGE;
if (fe->chkfd < 0)
continue;
nread2 = pread(fe->chkfd, chkbuf, chunksize, pos);
if (nread2 <= 0)
return -errno;
if (nread != nread2)
return -EFBIG;
if (memcmp(buf, chkbuf, nread)) {
fprintf(stderr, "%d[%u]/%u %s: %llu bytes mismatch @ %llu of %s\n",
getpid(), procid, sn, op, chunksize | 0ULL,
pos | 0ULL, fe->subpath);
baddump(sn, op, buf, chkbuf, nread);
return -EBADMSG;
}
}
return 0;
}
static int doscan_f(int op, unsigned int sn)
{
struct fent *fe;
uint64_t chunksize;
ssize_t fsz;
int ret;
fe = getfent(FT_REGm, random());
if (!fe)
return 0;
ret = tryopen(sn, __func__, fe);
if (ret)
return ret;
fsz = lseek(fe->fd, 0, SEEK_END);
if (fsz <= 0) {
if (!fsz) {
printf("%d[%u]/%u %s: zero size @ %s\n",
getpid(), procid, sn, __func__, fe->subpath);
return 0;
}
fprintf(stderr, "%d[%u]/%u %s: lseek %s failed %d\n",
getpid(), procid, sn, __func__, fe->subpath, errno);
return -errno;
}
chunksize = ((uint64_t)random() * random() % MAX_SCAN_CHUNKSIZE) + 1;
return __doscan_f(sn, __func__, fe, fsz, chunksize);
}
static int doscan_aligned_f(int op, unsigned int sn)
{
const int psz = getpagesize();
struct fent *fe;
uint64_t chunksize, maxchunksize;
ssize_t fsz;
int ret;
fe = getfent(FT_REGm, random());
if (!fe)
return 0;
ret = tryopen(sn, __func__, fe);
if (ret)
return ret;
fsz = lseek(fe->fd, 0, SEEK_END);
if (fsz <= psz) {
if (fsz >= 0) {
printf("%d[%u]/%u %s: size too small %lld @ %s\n",
getpid(), procid, sn, __func__, fsz | 0LL,
fe->subpath);
return 0;
}
fprintf(stderr, "%d[%u]/%u %s: lseek %s failed %d\n",
getpid(), procid, sn, __func__, fe->subpath, errno);
return -errno;
}
maxchunksize = (fsz - psz > MAX_SCAN_CHUNKSIZE ?
MAX_SCAN_CHUNKSIZE : fsz - psz);
chunksize = random() * random() % maxchunksize;
chunksize = (((chunksize - 1) / psz) + 1) * psz;
if (!chunksize)
chunksize = psz;
return __doscan_f(sn, __func__, fe, fsz, chunksize);
}
void randomdelay(void)
{
uint64_t lr = ((uint64_t) random() << 32) + random();
clock_t start;
clock_t length = (lr % CLOCKS_PER_SEC) >> 1;
start = clock();
while (clock() < start + length)
(void)sched_yield();
}
void sg_handler(int signum)
{
switch (signum) {
case SIGTERM:
should_stop = 1;
break;
default:
break;
}
}
struct opdesc ops[] = {
[OP_GETDENTS] = { "getdents", getdents_f, 5, false },
[OP_READLINK] = { "readlink", readlink_f, 5, false },
[OP_SEQREAD_ALIGNED] = { "readscan_aligned", doscan_aligned_f, 10, false },
[OP_SEQREAD_UNALIGNED] = { "readscan_unaligned", doscan_f, 10, false },
[OP_READ] = { "read", read_f, 30, false},
[OP_FADVISE] = { "fadvise", fadvise_f, 3, false},
[OP_DROP_CACHES] = { "drop_caches", drop_caches_f, 1, true},
};
static int parse_options(int argc, char *argv[])
{
char *testdir, *chkdir;
int opt;
while ((opt = getopt(argc, argv, "d:l:p:s:")) != -1) {
switch (opt) {
case 'l':
loops = atoi(optarg);
if (loops < 0) {
fprintf(stderr, "invalid loops %d\n", loops);
return -EINVAL;
}
break;
case 'p':
nprocs = atoi(optarg);
if (nprocs < 0) {
fprintf(stderr, "invalid workers %d\n",
nprocs);
return -EINVAL;
}
break;
case 's':
r_seed = atoi(optarg);
if (r_seed < 0) {
fprintf(stderr, "invalid random seed %d\n",
r_seed);
return -EINVAL;
}
break;
case 'd':
if (!*optarg) {
fprintf(stderr, "invalid dump file\n");
return -EINVAL;
}
dumpfile = optarg;
break;
default: /* '?' */
return -EINVAL;
}
}
if (optind >= argc)
return -EINVAL;
testdir = argv[optind++];
if (testdir) {
testdir_fd = open(testdir, O_PATH);
if (testdir_fd < 0) {
fprintf(stderr, "cannot open testdir fd @ %s: %s\n",
testdir, strerror(errno));
return 1;
}
}
if (argc > optind) {
chkdir = argv[optind++];
chkdir_fd = open(chkdir, O_PATH);
if (chkdir_fd < 0) {
fprintf(stderr, "cannot open checkdir fd @ %s: %s\n",
chkdir, strerror(errno));
return 1;
}
}
return 0;
}
static void usage(void)
{
fputs("usage: [options] TESTDIR [COMPRDIR]\n\n"
"Stress test for EROFS filesystem, where TESTDIR is the directory to test and\n"
"COMPRDIR (optional) serves as a directory for data comparison.\n"
" -l# Number of times each worker should loop (0 for infinite, default: 1)\n"
" -p# Number of parallel worker processes (default: 1)\n"
" -s# Seed for random generator (default: random)\n"
" -d<file> Specify a dumpfile for the inconsistent data\n",
stderr);
}
unsigned int *freq_table;
int freq_table_size;
static void doproc(void)
{
unsigned int sn;
srandom(r_seed + procid);
for (sn = 0; !should_stop && (!loops || sn < loops); ++sn) {
int op, err;
op = freq_table[random() % freq_table_size];
if (op >= ARRAY_SIZE(ops)) {
fprintf(stderr, "%d[%u]/%u %s: internal error\n",
getpid(), procid, sn, __func__);
abort();
}
if (sn && op != OP_DROP_CACHES)
randomdelay();
err = ops[op].func(op, sn);
if (err) {
fprintf(stderr, "%d[%u]/%u test failed (%d): %s\n",
getpid(), procid, sn, err, strerror(-err));
exit(1);
}
}
}
static void make_freq_table(void)
{
int f, i;
struct opdesc *p;
for (p = ops, f = 0; p < ops + ARRAY_SIZE(ops); p++) {
if (!superuser && p->requireroot)
continue;
f += p->freq;
}
freq_table = malloc(f * sizeof(*freq_table));
freq_table_size = f;
for (p = ops, i = 0; p < ops + ARRAY_SIZE(ops); p++) {
if (!superuser && p->requireroot)
continue;
for (f = 0; f < p->freq; f++, i++)
freq_table[i] = p - ops;
}
}
int main(int argc, char *argv[])
{
unsigned int i;
int err, stat;
struct sigaction action;
err = parse_options(argc, argv);
if (err) {
if (err == -EINVAL)
usage();
return 1;
}
err = init_filetable(testdir_fd);
if (err) {
fprintf(stderr, "cannot initialize file table: %s\n",
strerror(errno));
return 1;
}
superuser = (geteuid() == 0);
setpgid(0, 0);
action.sa_handler = sg_handler;
action.sa_flags = 0;
if (sigaction(SIGTERM, &action, 0)) {
perror("sigaction failed");
exit(1);
}
if (!r_seed)
r_seed = (time(NULL) ? : 1);
make_freq_table();
/* spawn nprocs processes */
for (i = 0; i < nprocs; ++i) {
if (fork() == 0) {
action.sa_handler = SIG_DFL;
sigemptyset(&action.sa_mask);
if (sigaction(SIGTERM, &action, 0)) {
perror("sigaction failed");
exit(1);
}
procid = i;
doproc();
return 0;
}
}
err = 0;
while (wait(&stat) > 0 && !should_stop) {
if (!WIFEXITED(stat)) {
err = 1;
break;
}
if (WEXITSTATUS(stat)) {
err = WEXITSTATUS(stat);
break;
}
}
action.sa_flags = SA_RESTART;
sigaction(SIGTERM, &action, 0);
kill(-getpid(), SIGTERM);
/* wait until all children exit */
while (wait(&stat) > 0)
continue;
return err;
}