aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoanne Koong <joannelkoong@gmail.com>2024-08-23 13:11:09 -0700
committerBernd Schubert <bernd@bsbernd.com>2025-07-16 01:46:09 +0200
commitb507cbc2b1aaec1931642497edcb6723a0d24dc4 (patch)
tree9b7556bbd548d3bee38d2f624876dba977014d45
parent3be844147764b96496bcae6d92fa4b0e43ebff42 (diff)
downloadlibfuse-b507cbc2b1aaec1931642497edcb6723a0d24dc4.tar.gz
Add statx support
This commit adds libfuse support for FUSE_STATX requests on linux distributions. Currently, statx is only supported on linux. To make the interface a ergonomic as possible (eg using native 'struct statx' vs 'struct fuse_statx'), this implementation gates the 'struct statx' changes by #ifdef linux. Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
-rw-r--r--ChangeLog.rst2
-rw-r--r--example/memfs_ll.cc3
-rw-r--r--example/passthrough.c21
-rw-r--r--example/passthrough_fh.c21
-rw-r--r--example/passthrough_ll.c25
-rw-r--r--include/fuse.h18
-rw-r--r--include/fuse_lowlevel.h35
-rw-r--r--lib/fuse.c108
-rw-r--r--lib/fuse_lowlevel.c66
-rw-r--r--lib/fuse_versionscript3
-rw-r--r--lib/modules/iconv.c19
-rw-r--r--lib/modules/subdir.c19
-rw-r--r--meson.build4
-rw-r--r--test/test_syscalls.c49
14 files changed, 391 insertions, 2 deletions
diff --git a/ChangeLog.rst b/ChangeLog.rst
index 658f898..505d9db 100644
--- a/ChangeLog.rst
+++ b/ChangeLog.rst
@@ -1,3 +1,5 @@
+libfuse 3.18
+
libfuse 3.17.1-rc0 (2024-02.10)
===============================
diff --git a/example/memfs_ll.cc b/example/memfs_ll.cc
index b4f0b63..0da7c25 100644
--- a/example/memfs_ll.cc
+++ b/example/memfs_ll.cc
@@ -1094,6 +1094,9 @@ static const struct fuse_lowlevel_ops memfs_oper = {
.copy_file_range = nullptr,
.lseek = nullptr,
.tmpfile = nullptr,
+#ifdef HAVE_STATX
+ .statx = nullptr,
+#endif
};
int main(int argc, char *argv[])
diff --git a/example/passthrough.c b/example/passthrough.c
index f0e686d..fdaa19e 100644
--- a/example/passthrough.c
+++ b/example/passthrough.c
@@ -519,6 +519,24 @@ static off_t xmp_lseek(const char *path, off_t off, int whence, struct fuse_file
return res;
}
+#ifdef HAVE_STATX
+static int xmp_statx(const char *path, int flags, int mask, struct statx *stxbuf,
+ struct fuse_file_info *fi)
+{
+ int fd = -1;
+ int res;
+
+ if (fi)
+ fd = fi->fh;
+
+ res = statx(fd, path, flags | AT_SYMLINK_NOFOLLOW, mask, stxbuf);
+ if (res == -1)
+ return -errno;
+
+ return 0;
+}
+#endif
+
static const struct fuse_operations xmp_oper = {
.init = xmp_init,
.getattr = xmp_getattr,
@@ -556,6 +574,9 @@ static const struct fuse_operations xmp_oper = {
.copy_file_range = xmp_copy_file_range,
#endif
.lseek = xmp_lseek,
+#ifdef HAVE_STATX
+ .statx = xmp_statx,
+#endif
};
int main(int argc, char *argv[])
diff --git a/example/passthrough_fh.c b/example/passthrough_fh.c
index 6764554..0d4fb5b 100644
--- a/example/passthrough_fh.c
+++ b/example/passthrough_fh.c
@@ -616,6 +616,24 @@ static off_t xmp_lseek(const char *path, off_t off, int whence, struct fuse_file
return res;
}
+#ifdef HAVE_STATX
+static int xmp_statx(const char *path, int flags, int mask, struct statx *stxbuf,
+ struct fuse_file_info *fi)
+{
+ int fd = -1;
+ int res;
+
+ if (fi)
+ fd = fi->fh;
+
+ res = statx(fd, path, flags | AT_SYMLINK_NOFOLLOW, mask, stxbuf);
+ if (res == -1)
+ return -errno;
+
+ return 0;
+}
+#endif
+
static const struct fuse_operations xmp_oper = {
.init = xmp_init,
.getattr = xmp_getattr,
@@ -662,6 +680,9 @@ static const struct fuse_operations xmp_oper = {
.copy_file_range = xmp_copy_file_range,
#endif
.lseek = xmp_lseek,
+#ifdef HAVE_STATX
+ .statx = xmp_statx,
+#endif
};
int main(int argc, char *argv[])
diff --git a/example/passthrough_ll.c b/example/passthrough_ll.c
index 9e027b4..5ca6efa 100644
--- a/example/passthrough_ll.c
+++ b/example/passthrough_ll.c
@@ -1215,6 +1215,28 @@ static void lo_lseek(fuse_req_t req, fuse_ino_t ino, off_t off, int whence,
fuse_reply_err(req, errno);
}
+#ifdef HAVE_STATX
+static void lo_statx(fuse_req_t req, fuse_ino_t ino, int flags, int mask,
+ struct fuse_file_info *fi)
+{
+ struct lo_data *lo = lo_data(req);
+ struct statx buf;
+ int res;
+ int fd;
+
+ if (fi)
+ fd = fi->fh;
+ else
+ fd = lo_fd(req, ino);
+
+ res = statx(fd, "", flags | AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW, mask, &buf);
+ if (res == -1)
+ fuse_reply_err(req, errno);
+ else
+ fuse_reply_statx(req, 0, &buf, lo->timeout);
+}
+#endif
+
static const struct fuse_lowlevel_ops lo_oper = {
.init = lo_init,
.destroy = lo_destroy,
@@ -1255,6 +1277,9 @@ static const struct fuse_lowlevel_ops lo_oper = {
.copy_file_range = lo_copy_file_range,
#endif
.lseek = lo_lseek,
+#ifdef HAVE_STATX
+ .statx = lo_statx,
+#endif
};
int main(int argc, char *argv[])
diff --git a/include/fuse.h b/include/fuse.h
index b990043..06feacb 100644
--- a/include/fuse.h
+++ b/include/fuse.h
@@ -33,6 +33,9 @@ extern "C" {
* Basic FUSE API *
* ----------------------------------------------------------- */
+/* Forward declaration */
+struct statx;
+
/** Handle for a FUSE filesystem */
struct fuse;
@@ -850,6 +853,19 @@ struct fuse_operations {
* Find next data or hole after the specified offset
*/
off_t (*lseek) (const char *, off_t off, int whence, struct fuse_file_info *);
+
+#ifdef HAVE_STATX
+ /**
+ * Get extended file attributes.
+ *
+ * fi may be NULL.
+ *
+ * If path is NULL, then the AT_EMPTY_PATH bit in flags will be
+ * already set.
+ */
+ int (*statx)(const char *path, int flags, int mask, struct statx *stxbuf,
+ struct fuse_file_info *fi);
+#endif
};
/** Extra context that may be needed by some filesystems
@@ -1344,6 +1360,8 @@ ssize_t fuse_fs_copy_file_range(struct fuse_fs *fs, const char *path_in,
size_t len, int flags);
off_t fuse_fs_lseek(struct fuse_fs *fs, const char *path, off_t off, int whence,
struct fuse_file_info *fi);
+int fuse_fs_statx(struct fuse_fs *fs, const char *path, int flags, int mask,
+ struct statx *stxbuf, struct fuse_file_info *fi);
void fuse_fs_init(struct fuse_fs *fs, struct fuse_conn_info *conn,
struct fuse_config *cfg);
void fuse_fs_destroy(struct fuse_fs *fs);
diff --git a/include/fuse_lowlevel.h b/include/fuse_lowlevel.h
index 75e084d..844ee71 100644
--- a/include/fuse_lowlevel.h
+++ b/include/fuse_lowlevel.h
@@ -49,6 +49,9 @@ typedef uint64_t fuse_ino_t;
/** Request pointer type */
typedef struct fuse_req *fuse_req_t;
+/* Forward declaration */
+struct statx;
+
/**
* Session
*
@@ -1303,7 +1306,6 @@ struct fuse_lowlevel_ops {
void (*lseek) (fuse_req_t req, fuse_ino_t ino, off_t off, int whence,
struct fuse_file_info *fi);
-
/**
* Create a tempfile
*
@@ -1325,6 +1327,23 @@ struct fuse_lowlevel_ops {
void (*tmpfile) (fuse_req_t req, fuse_ino_t parent,
mode_t mode, struct fuse_file_info *fi);
+#ifdef HAVE_STATX
+ /**
+ * Get extended file attributes.
+ *
+ * Valid replies:
+ * fuse_reply_statx
+ * fuse_reply_err
+ *
+ * @param req request handle
+ * @param ino the inode number
+ * @param flags bitmask of requested flags
+ * @param mask bitmask of requested fields
+ * @param fi file information (may be NULL)
+ */
+ void (*statx)(fuse_req_t req, fuse_ino_t ino, int flags, int mask,
+ struct fuse_file_info *fi);
+#endif
};
/**
@@ -1705,6 +1724,20 @@ int fuse_reply_poll(fuse_req_t req, unsigned revents);
*/
int fuse_reply_lseek(fuse_req_t req, off_t off);
+/**
+ * Reply with extended file attributes.
+ *
+ * Possible requests:
+ * statx
+ *
+ * @param req request handle
+ * @param flags statx flags
+ * @param statx the attributes
+ * @param attr_timeout validity timeout (in seconds) for the attributes
+ * @return zero for success, -errno for failure to send reply
+ */
+int fuse_reply_statx(fuse_req_t req, int flags, struct statx *statx, double attr_timeout);
+
/* ----------------------------------------------------------- *
* Notification *
* ----------------------------------------------------------- */
diff --git a/lib/fuse.c b/lib/fuse.c
index 68b61ce..333fdc7 100644
--- a/lib/fuse.c
+++ b/lib/fuse.c
@@ -1507,6 +1507,29 @@ static void set_stat(struct fuse *f, fuse_ino_t nodeid, struct stat *stbuf)
stbuf->st_gid = f->conf.gid;
}
+#ifdef HAVE_STATX
+static void set_statx(struct fuse *f, fuse_ino_t nodeid, struct statx *stxbuf)
+{
+ if (!f->conf.use_ino)
+ stxbuf->stx_ino = nodeid;
+ if (f->conf.set_mode) {
+ if (f->conf.dmask && S_ISDIR(stxbuf->stx_mode))
+ stxbuf->stx_mode = (stxbuf->stx_mode & S_IFMT) |
+ (0777 & ~f->conf.dmask);
+ else if (f->conf.fmask)
+ stxbuf->stx_mode = (stxbuf->stx_mode & S_IFMT) |
+ (0777 & ~f->conf.fmask);
+ else
+ stxbuf->stx_mode = (stxbuf->stx_mode & S_IFMT) |
+ (0777 & ~f->conf.umask);
+ }
+ if (f->conf.set_uid)
+ stxbuf->stx_uid = f->conf.uid;
+ if (f->conf.set_gid)
+ stxbuf->stx_gid = f->conf.gid;
+}
+#endif
+
static struct fuse *req_fuse(fuse_req_t req)
{
return (struct fuse *) fuse_req_userdata(req);
@@ -2312,6 +2335,39 @@ off_t fuse_fs_lseek(struct fuse_fs *fs, const char *path, off_t off, int whence,
return fs->op.lseek(path, off, whence, fi);
}
+#ifdef HAVE_STATX
+int fuse_fs_statx(struct fuse_fs *fs, const char *path, int flags, int mask,
+ struct statx *stxbuf, struct fuse_file_info *fi)
+{
+ fuse_get_context()->private_data = fs->user_data;
+ if (fs->op.statx) {
+ if (fs->debug) {
+ char buf[10];
+
+ fuse_log(FUSE_LOG_DEBUG, "statx[%s] %s %d %d\n",
+ file_info_string(fi, buf, sizeof(buf)), path,
+ flags, mask);
+ }
+ return fs->op.statx(path, flags, mask, stxbuf, fi);
+ }
+
+ return -ENOSYS;
+}
+#else
+int fuse_fs_statx(struct fuse_fs *fs, const char *path, int flags, int mask,
+ struct statx *stxbuf, struct fuse_file_info *fi)
+{
+ (void)fs;
+ (void)path;
+ (void)flags;
+ (void)mask;
+ (void)stxbuf;
+ (void)fi;
+
+ return -ENOSYS;
+}
+#endif
+
static int is_open(struct fuse *f, fuse_ino_t dir, const char *name)
{
struct node *node;
@@ -4361,6 +4417,55 @@ static void fuse_lib_lseek(fuse_req_t req, fuse_ino_t ino, off_t off, int whence
reply_err(req, res);
}
+#ifdef HAVE_STATX
+static void fuse_lib_statx(fuse_req_t req, fuse_ino_t ino, int flags, int mask,
+ struct fuse_file_info *fi)
+{
+ struct fuse *f = req_fuse_prepare(req);
+ struct statx stxbuf;
+ char *path;
+ int err;
+
+ memset(&stxbuf, 0, sizeof(stxbuf));
+
+ if (fi != NULL)
+ err = get_path_nullok(f, ino, &path);
+ else
+ err = get_path(f, ino, &path);
+
+ if (!err) {
+ struct fuse_intr_data d;
+
+ if (!path)
+ flags |= AT_EMPTY_PATH;
+ fuse_prepare_interrupt(f, req, &d);
+ err = fuse_fs_statx(f->fs, path, flags, mask, &stxbuf, fi);
+ fuse_finish_interrupt(f, req, &d);
+ free_path(f, ino, path);
+ }
+ if (!err) {
+ struct node *node;
+
+ pthread_mutex_lock(&f->lock);
+ node = get_node(f, ino);
+ if (node->is_hidden && stxbuf.stx_nlink > 0)
+ stxbuf.stx_nlink--;
+ if (f->conf.auto_cache) {
+ struct stat stbuf;
+
+ stbuf.st_mtime = stxbuf.stx_mtime.tv_nsec;
+ ST_MTIM_NSEC(&stbuf) = stxbuf.stx_mtime.tv_nsec;
+ stbuf.st_size = stxbuf.stx_size;
+ update_stat(node, &stbuf);
+ }
+ pthread_mutex_unlock(&f->lock);
+ set_statx(f, ino, &stxbuf);
+ fuse_reply_statx(req, 0, &stxbuf, f->conf.attr_timeout);
+ } else
+ reply_err(req, err);
+}
+#endif
+
static int clean_delay(struct fuse *f)
{
/*
@@ -4459,6 +4564,9 @@ static struct fuse_lowlevel_ops fuse_path_ops = {
.fallocate = fuse_lib_fallocate,
.copy_file_range = fuse_lib_copy_file_range,
.lseek = fuse_lib_lseek,
+#ifdef HAVE_STATX
+ .statx = fuse_lib_statx,
+#endif
};
int fuse_notify_poll(struct fuse_pollhandle *ph)
diff --git a/lib/fuse_lowlevel.c b/lib/fuse_lowlevel.c
index 91f4244..1ae3473 100644
--- a/lib/fuse_lowlevel.c
+++ b/lib/fuse_lowlevel.c
@@ -1216,6 +1216,33 @@ int fuse_reply_lseek(fuse_req_t req, off_t off)
return send_reply_ok(req, &arg, sizeof(arg));
}
+#ifdef HAVE_STATX
+int fuse_reply_statx(fuse_req_t req, int flags, struct statx *statx,
+ double attr_timeout)
+{
+ struct fuse_statx_out arg;
+
+ memset(&arg, 0, sizeof(arg));
+ arg.flags = flags;
+ arg.attr_valid = calc_timeout_sec(attr_timeout);
+ arg.attr_valid_nsec = calc_timeout_nsec(attr_timeout);
+ memcpy(&arg.stat, statx, sizeof(arg.stat));
+
+ return send_reply_ok(req, &arg, sizeof(arg));
+}
+#else
+int fuse_reply_statx(fuse_req_t req, int flags, struct statx *statx,
+ double attr_timeout)
+{
+ (void)req;
+ (void)flags;
+ (void)statx;
+ (void)attr_timeout;
+
+ return -ENOSYS;
+}
+#endif
+
static void _do_lookup(fuse_req_t req, const fuse_ino_t nodeid,
const void *op_in, const void *in_payload)
{
@@ -2428,6 +2455,42 @@ static void do_lseek(fuse_req_t req, const fuse_ino_t nodeid, const void *inarg)
_do_lseek(req, nodeid, inarg, NULL);
}
+#ifdef HAVE_STATX
+static void _do_statx(fuse_req_t req, const fuse_ino_t nodeid,
+ const void *op_in, const void *in_payload)
+{
+ (void)in_payload;
+ const struct fuse_statx_in *arg = op_in;
+ struct fuse_file_info *fip = NULL;
+ struct fuse_file_info fi;
+
+ if (arg->getattr_flags & FUSE_GETATTR_FH) {
+ memset(&fi, 0, sizeof(fi));
+ fi.fh = arg->fh;
+ fip = &fi;
+ }
+
+ if (req->se->op.statx)
+ req->se->op.statx(req, nodeid, arg->sx_flags, arg->sx_mask, fip);
+ else
+ fuse_reply_err(req, ENOSYS);
+}
+#else
+static void _do_statx(fuse_req_t req, const fuse_ino_t nodeid,
+ const void *op_in, const void *in_payload)
+{
+ (void)in_payload;
+ (void)req;
+ (void)nodeid;
+ (void)op_in;
+}
+#endif
+
+static void do_statx(fuse_req_t req, fuse_ino_t nodeid, const void *inarg)
+{
+ _do_statx(req, nodeid, inarg, NULL);
+}
+
static bool want_flags_valid(uint64_t capable, uint64_t want)
{
uint64_t unknown_flags = want & (~capable);
@@ -2510,7 +2573,6 @@ bool fuse_get_feature_flag(struct fuse_conn_info *conn,
return conn->capable_ext & flag ? true : false;
}
-
/* Prevent bogus data races (bogus since "init" is called before
* multi-threading becomes relevant */
static __attribute__((no_sanitize("thread"))) void
@@ -3297,6 +3359,7 @@ static struct {
[FUSE_RENAME2] = { do_rename2, "RENAME2" },
[FUSE_COPY_FILE_RANGE] = { do_copy_file_range, "COPY_FILE_RANGE" },
[FUSE_LSEEK] = { do_lseek, "LSEEK" },
+ [FUSE_STATX] = { do_statx, "STATX" },
[CUSE_INIT] = { cuse_lowlevel_init, "CUSE_INIT" },
};
@@ -3351,6 +3414,7 @@ static struct {
[FUSE_RENAME2] = { _do_rename2, "RENAME2" },
[FUSE_COPY_FILE_RANGE] = { _do_copy_file_range, "COPY_FILE_RANGE" },
[FUSE_LSEEK] = { _do_lseek, "LSEEK" },
+ [FUSE_STATX] = { _do_statx, "STATX" },
[CUSE_INIT] = { _cuse_lowlevel_init, "CUSE_INIT" },
};
diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript
index 2d8884d..0e581f1 100644
--- a/lib/fuse_versionscript
+++ b/lib/fuse_versionscript
@@ -212,6 +212,9 @@ FUSE_3.18 {
# Not part of public API, for internal test use only
fuse_convert_to_conn_want_ext;
+
+ fuse_reply_statx;
+ fuse_fs_statx;
} FUSE_3.17;
# Local Variables:
diff --git a/lib/modules/iconv.c b/lib/modules/iconv.c
index 599b8df..417c904 100644
--- a/lib/modules/iconv.c
+++ b/lib/modules/iconv.c
@@ -568,6 +568,22 @@ static off_t iconv_lseek(const char *path, off_t off, int whence,
return res;
}
+#ifdef HAVE_STATX
+static int iconv_statx(const char *path, int flags, int mask, struct statx *stxbuf,
+ struct fuse_file_info *fi)
+{
+ struct iconv *ic = iconv_get();
+ char *newpath;
+ int res = iconv_convpath(ic, path, &newpath, 0);
+
+ if (!res) {
+ res = fuse_fs_statx(ic->next, newpath, flags, mask, stxbuf, fi);
+ free(newpath);
+ }
+ return res;
+}
+#endif
+
static void *iconv_init(struct fuse_conn_info *conn,
struct fuse_config *cfg)
{
@@ -627,6 +643,9 @@ static const struct fuse_operations iconv_oper = {
.flock = iconv_flock,
.bmap = iconv_bmap,
.lseek = iconv_lseek,
+#ifdef HAVE_STATX
+ .statx = iconv_statx,
+#endif
};
static const struct fuse_opt iconv_opts[] = {
diff --git a/lib/modules/subdir.c b/lib/modules/subdir.c
index dd2d49d..67c4697 100644
--- a/lib/modules/subdir.c
+++ b/lib/modules/subdir.c
@@ -553,6 +553,22 @@ static off_t subdir_lseek(const char *path, off_t off, int whence,
return res;
}
+#ifdef HAVE_STATX
+static int subdir_statx(const char *path, int flags, int mask, struct statx *stxbuf,
+ struct fuse_file_info *fi)
+{
+ struct subdir *ic = subdir_get();
+ char *newpath;
+ int res = subdir_addpath(ic, path, &newpath);
+
+ if (!res) {
+ res = fuse_fs_statx(ic->next, newpath, flags, mask, stxbuf, fi);
+ free(newpath);
+ }
+ return res;
+}
+#endif
+
static void *subdir_init(struct fuse_conn_info *conn,
struct fuse_config *cfg)
{
@@ -608,6 +624,9 @@ static const struct fuse_operations subdir_oper = {
.flock = subdir_flock,
.bmap = subdir_bmap,
.lseek = subdir_lseek,
+#ifdef HAVE_STATX
+ .statx = subdir_statx,
+#endif
};
static const struct fuse_opt subdir_opts[] = {
diff --git a/meson.build b/meson.build
index 7091d5f..64ff334 100644
--- a/meson.build
+++ b/meson.build
@@ -126,6 +126,10 @@ private_cfg.set('HAVE_ICONV',
cc.has_function('iconv', prefix: '#include <iconv.h>'))
private_cfg.set('HAVE_BACKTRACE',
cc.has_function('backtrace', prefix: '#include <execinfo.h>'))
+public_cfg.set('HAVE_STATX',
+ cc.has_function('statx', prefix : '#define _GNU_SOURCE\n#include <sys/stat.h>'))
+private_cfg.set('HAVE_STATX',
+ cc.has_function('statx', prefix : '#define _GNU_SOURCE\n#include <sys/stat.h>'))
# Struct member checks
private_cfg.set('HAVE_STRUCT_STAT_ST_ATIM',
diff --git a/test/test_syscalls.c b/test/test_syscalls.c
index 4bbe973..61ee953 100644
--- a/test/test_syscalls.c
+++ b/test/test_syscalls.c
@@ -11,6 +11,7 @@
#include <utime.h>
#include <errno.h>
#include <assert.h>
+#include <time.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
@@ -921,6 +922,53 @@ static int test_copy_file_range(void)
}
#endif
+#ifdef HAVE_STATX
+static int test_statx(void)
+{
+ struct statx sb;
+ char msg[] = "hi";
+ size_t msg_size = sizeof(msg);
+ struct timespec tp;
+ int res;
+
+ memset(&sb, 0, sizeof(sb));
+ unlink(testfile);
+
+ start_test("statx");
+
+ res = create_testfile(testfile, msg, msg_size);
+ if (res == -1)
+ return -1;
+
+ res = statx(-1, testfile, AT_EMPTY_PATH,
+ STATX_BASIC_STATS | STATX_BTIME, &sb);
+ if (res == -1)
+ return -1;
+
+ if (sb.stx_size != msg_size)
+ return -1;
+
+ clock_gettime(CLOCK_REALTIME, &tp);
+
+ if (sb.stx_btime.tv_sec > tp.tv_sec)
+ return -1;
+
+ if (sb.stx_btime.tv_sec == tp.tv_sec &&
+ sb.stx_btime.tv_nsec >= tp.tv_nsec)
+ return -1;
+
+ unlink(testfile);
+
+ success();
+ return 0;
+}
+#else
+static int test_statx(void)
+{
+ return 0;
+}
+#endif
+
static int test_utime(void)
{
struct utimbuf utm;
@@ -2179,6 +2227,7 @@ int main(int argc, char *argv[])
err += test_create_ro_dir(O_CREAT | O_WRONLY);
err += test_create_ro_dir(O_CREAT | O_TRUNC);
err += test_copy_file_range();
+ err += test_statx();
#ifndef __FreeBSD__
err += test_create_tmpfile();
err += test_create_and_link_tmpfile();