aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmd/sharefs
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/sharefs')
-rw-r--r--cmd/sharefs/fuse-operations.c414
-rw-r--r--cmd/sharefs/fuse-operations.h44
-rw-r--r--cmd/sharefs/testsuite/main.go119
-rw-r--r--cmd/sharefs/testsuite/raceattr.go122
4 files changed, 493 insertions, 206 deletions
diff --git a/cmd/sharefs/fuse-operations.c b/cmd/sharefs/fuse-operations.c
index ecff1ad6..36417126 100644
--- a/cmd/sharefs/fuse-operations.c
+++ b/cmd/sharefs/fuse-operations.c
@@ -14,269 +14,301 @@
/* MUST_TRANSLATE_PATHNAME translates a userspace pathname to a relative pathname;
* the resulting address points to a constant string or part of pathname, it is never heap allocated. */
#define MUST_TRANSLATE_PATHNAME(pathname) \
- do { \
- if (pathname == NULL) \
- return -EINVAL; \
- while (*pathname == '/') \
- pathname++; \
- if (*pathname == '\0') \
- pathname = "."; \
- } while (0)
+ do { \
+ if (pathname == NULL) \
+ return -EINVAL; \
+ while (*pathname == '/') \
+ pathname++; \
+ if (*pathname == '\0') \
+ pathname = "."; \
+ } while (0)
/* GET_CONTEXT_PRIV obtains fuse context and private data for the calling thread. */
-#define GET_CONTEXT_PRIV(ctx, priv) \
- do { \
- ctx = fuse_get_context(); \
- priv = ctx->private_data; \
- } while (0)
+#define GET_CONTEXT_PRIV(ctx, priv) \
+ do { \
+ ctx = fuse_get_context(); \
+ priv = ctx->private_data; \
+ } while (0)
/* impl_getattr modifies a struct stat from the kernel to present to userspace;
* impl_getattr returns a negative errno style error code. */
-static int impl_getattr(struct fuse_context *ctx, struct stat *statbuf) {
- /* allowlist of permitted types */
- if (!S_ISDIR(statbuf->st_mode) && !S_ISREG(statbuf->st_mode) && !S_ISLNK(statbuf->st_mode)) {
- return -ENOTRECOVERABLE; /* returning an errno causes all operations on the file to return EIO */
- }
+static int impl_getattr(struct fuse_context *ctx, struct stat *statbuf)
+{
+ /* allowlist of permitted types */
+ if (!S_ISDIR(statbuf->st_mode) && !S_ISREG(statbuf->st_mode) &&
+ !S_ISLNK(statbuf->st_mode)) {
+ return -ENOTRECOVERABLE; /* returning an errno causes all operations on the file to return EIO */
+ }
#define OVERRIDE_PERM(v) (statbuf->st_mode & ~0777) | (v & 0777)
- if (S_ISDIR(statbuf->st_mode))
- statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_DIR);
- else if (S_ISREG(statbuf->st_mode))
- statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_REG);
- else
- statbuf->st_mode = 0; /* should always be symlink in this case */
+ if (S_ISDIR(statbuf->st_mode))
+ statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_DIR);
+ else if (S_ISREG(statbuf->st_mode))
+ statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_REG);
+ else
+ statbuf->st_mode =
+ 0; /* should always be symlink in this case */
- statbuf->st_uid = ctx->uid;
- statbuf->st_gid = SHAREFS_MEDIA_RW_ID;
- statbuf->st_ctim = statbuf->st_mtim;
- statbuf->st_nlink = 1;
- return 0;
+ statbuf->st_uid = ctx->uid;
+ statbuf->st_gid = SHAREFS_MEDIA_RW_ID;
+ statbuf->st_ctim = statbuf->st_mtim;
+ statbuf->st_nlink = 1;
+ return 0;
}
/* fuse_operations implementation */
-int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi) {
- struct fuse_context *ctx;
- struct sharefs_private *priv;
- GET_CONTEXT_PRIV(ctx, priv);
- MUST_TRANSLATE_PATHNAME(pathname);
+int sharefs_getattr(const char *pathname, struct stat *statbuf,
+ struct fuse_file_info *fi)
+{
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
- (void)fi;
+ (void)fi;
- if (fstatat(priv->dirfd, pathname, statbuf, AT_SYMLINK_NOFOLLOW) == -1)
- return -errno;
- return impl_getattr(ctx, statbuf);
+ if (fstatat(priv->dirfd, pathname, statbuf, AT_SYMLINK_NOFOLLOW) == -1)
+ return -errno;
+ return impl_getattr(ctx, statbuf);
}
-int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags) {
- int fd;
- DIR *dp;
- struct stat st;
- int ret = 0;
- struct dirent *de;
+int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler,
+ off_t offset, struct fuse_file_info *fi,
+ enum fuse_readdir_flags flags)
+{
+ int fd;
+ DIR *dp;
+ struct stat st;
+ int ret = 0;
+ struct dirent *de;
- struct fuse_context *ctx;
- struct sharefs_private *priv;
- GET_CONTEXT_PRIV(ctx, priv);
- MUST_TRANSLATE_PATHNAME(pathname);
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
- (void)offset;
- (void)fi;
+ (void)offset;
+ (void)fi;
- if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_DIRECTORY | O_CLOEXEC)) == -1)
- return -errno;
- if ((dp = fdopendir(fd)) == NULL) {
- close(fd);
- return -errno;
- }
+ if ((fd = openat(priv->dirfd, pathname,
+ O_RDONLY | O_DIRECTORY | O_CLOEXEC)) == -1)
+ return -errno;
+ if ((dp = fdopendir(fd)) == NULL) {
+ close(fd);
+ return -errno;
+ }
- errno = 0; /* for the next readdir call */
- while ((de = readdir(dp)) != NULL) {
- if (flags & FUSE_READDIR_PLUS) {
- if (fstatat(dirfd(dp), de->d_name, &st, AT_SYMLINK_NOFOLLOW) == -1) {
- ret = -errno;
- break;
- }
+ errno = 0; /* for the next readdir call */
+ while ((de = readdir(dp)) != NULL) {
+ if (flags & FUSE_READDIR_PLUS) {
+ if (fstatat(dirfd(dp), de->d_name, &st,
+ AT_SYMLINK_NOFOLLOW) == -1) {
+ ret = -errno;
+ break;
+ }
- if ((ret = impl_getattr(ctx, &st)) < 0)
- break;
+ if ((ret = impl_getattr(ctx, &st)) < 0)
+ break;
- errno = 0;
- ret = filler(buf, de->d_name, &st, 0, FUSE_FILL_DIR_PLUS);
- } else
- ret = filler(buf, de->d_name, NULL, 0, 0);
+ errno = 0;
+ ret = filler(buf, de->d_name, &st, 0,
+ FUSE_FILL_DIR_PLUS);
+ } else
+ ret = filler(buf, de->d_name, NULL, 0, 0);
- if (ret != 0) {
- ret = errno != 0 ? -errno : -EIO; /* filler */
- break;
- }
+ if (ret != 0) {
+ ret = errno != 0 ? -errno : -EIO; /* filler */
+ break;
+ }
- errno = 0; /* for the next readdir call */
- }
- if (ret == 0 && errno != 0)
- ret = -errno; /* readdir */
+ errno = 0; /* for the next readdir call */
+ }
+ if (ret == 0 && errno != 0)
+ ret = -errno; /* readdir */
- closedir(dp);
- return ret;
+ closedir(dp);
+ return ret;
}
-int sharefs_mkdir(const char *pathname, mode_t mode) {
- struct fuse_context *ctx;
- struct sharefs_private *priv;
- GET_CONTEXT_PRIV(ctx, priv);
- MUST_TRANSLATE_PATHNAME(pathname);
+int sharefs_mkdir(const char *pathname, mode_t mode)
+{
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
- (void)mode;
+ (void)mode;
- if (mkdirat(priv->dirfd, pathname, SHAREFS_PERM_DIR) == -1)
- return -errno;
- return 0;
+ if (mkdirat(priv->dirfd, pathname, SHAREFS_PERM_DIR) == -1)
+ return -errno;
+ return 0;
}
-int sharefs_unlink(const char *pathname) {
- struct fuse_context *ctx;
- struct sharefs_private *priv;
- GET_CONTEXT_PRIV(ctx, priv);
- MUST_TRANSLATE_PATHNAME(pathname);
+int sharefs_unlink(const char *pathname)
+{
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
- if (unlinkat(priv->dirfd, pathname, 0) == -1)
- return -errno;
- return 0;
+ if (unlinkat(priv->dirfd, pathname, 0) == -1)
+ return -errno;
+ return 0;
}
-int sharefs_rmdir(const char *pathname) {
- struct fuse_context *ctx;
- struct sharefs_private *priv;
- GET_CONTEXT_PRIV(ctx, priv);
- MUST_TRANSLATE_PATHNAME(pathname);
+int sharefs_rmdir(const char *pathname)
+{
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
- if (unlinkat(priv->dirfd, pathname, AT_REMOVEDIR) == -1)
- return -errno;
- return 0;
+ if (unlinkat(priv->dirfd, pathname, AT_REMOVEDIR) == -1)
+ return -errno;
+ return 0;
}
-int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags) {
- struct fuse_context *ctx;
- struct sharefs_private *priv;
- GET_CONTEXT_PRIV(ctx, priv);
- MUST_TRANSLATE_PATHNAME(oldpath);
- MUST_TRANSLATE_PATHNAME(newpath);
+int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags)
+{
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(oldpath);
+ MUST_TRANSLATE_PATHNAME(newpath);
- /* TODO(ophestra): replace with wrapper after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
- if (syscall(__NR_renameat2, priv->dirfd, oldpath, priv->dirfd, newpath, flags) == -1)
- return -errno;
- return 0;
+ /* TODO(ophestra): replace with wrapper after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
+ if (syscall(__NR_renameat2, priv->dirfd, oldpath, priv->dirfd, newpath,
+ flags) == -1)
+ return -errno;
+ return 0;
}
-int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi) {
- int fd;
- int ret;
+int sharefs_truncate(const char *pathname, off_t length,
+ struct fuse_file_info *fi)
+{
+ int fd;
+ int ret;
- struct fuse_context *ctx;
- struct sharefs_private *priv;
- GET_CONTEXT_PRIV(ctx, priv);
- MUST_TRANSLATE_PATHNAME(pathname);
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
- (void)fi;
+ (void)fi;
- if ((fd = openat(priv->dirfd, pathname, O_WRONLY | O_CLOEXEC)) == -1)
- return -errno;
- if ((ret = ftruncate(fd, length)) == -1)
- ret = -errno;
- close(fd);
- return ret;
+ if ((fd = openat(priv->dirfd, pathname, O_WRONLY | O_CLOEXEC)) == -1)
+ return -errno;
+ if ((ret = ftruncate(fd, length)) == -1)
+ ret = -errno;
+ close(fd);
+ return ret;
}
-int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi) {
- struct fuse_context *ctx;
- struct sharefs_private *priv;
- GET_CONTEXT_PRIV(ctx, priv);
- MUST_TRANSLATE_PATHNAME(pathname);
+int sharefs_utimens(const char *pathname, const struct timespec times[2],
+ struct fuse_file_info *fi)
+{
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
- (void)fi;
+ (void)fi;
- if (utimensat(priv->dirfd, pathname, times, AT_SYMLINK_NOFOLLOW) == -1)
- return -errno;
- return 0;
+ if (utimensat(priv->dirfd, pathname, times, AT_SYMLINK_NOFOLLOW) == -1)
+ return -errno;
+ return 0;
}
-int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi) {
- int fd;
+int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi)
+{
+ int fd;
- struct fuse_context *ctx;
- struct sharefs_private *priv;
- GET_CONTEXT_PRIV(ctx, priv);
- MUST_TRANSLATE_PATHNAME(pathname);
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
- (void)mode;
+ (void)mode;
- if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS, SHAREFS_PERM_REG)) == -1)
- return -errno;
- fi->fh = fd;
- return 0;
+ if ((fd = openat(priv->dirfd, pathname,
+ fi->flags & ~SHAREFS_FORBIDDEN_FLAGS,
+ SHAREFS_PERM_REG)) == -1)
+ return -errno;
+ fi->fh = fd;
+ return 0;
}
-int sharefs_open(const char *pathname, struct fuse_file_info *fi) {
- int fd;
+int sharefs_open(const char *pathname, struct fuse_file_info *fi)
+{
+ int fd;
- struct fuse_context *ctx;
- struct sharefs_private *priv;
- GET_CONTEXT_PRIV(ctx, priv);
- MUST_TRANSLATE_PATHNAME(pathname);
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
- if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS)) == -1)
- return -errno;
- fi->fh = fd;
- return 0;
+ if ((fd = openat(priv->dirfd, pathname,
+ fi->flags & ~SHAREFS_FORBIDDEN_FLAGS)) == -1)
+ return -errno;
+ fi->fh = fd;
+ return 0;
}
-int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
- int ret;
+int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset,
+ struct fuse_file_info *fi)
+{
+ int ret;
- (void)pathname;
+ (void)pathname;
- if ((ret = pread(fi->fh, buf, count, offset)) == -1)
- return -errno;
- return ret;
+ if ((ret = pread(fi->fh, buf, count, offset)) == -1)
+ return -errno;
+ return ret;
}
-int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
- int ret;
+int sharefs_write(const char *pathname, const char *buf, size_t count,
+ off_t offset, struct fuse_file_info *fi)
+{
+ int ret;
- (void)pathname;
+ (void)pathname;
- if ((ret = pwrite(fi->fh, buf, count, offset)) == -1)
- return -errno;
- return ret;
+ if ((ret = pwrite(fi->fh, buf, count, offset)) == -1)
+ return -errno;
+ return ret;
}
-int sharefs_statfs(const char *pathname, struct statvfs *statbuf) {
- int fd;
- int ret;
+int sharefs_statfs(const char *pathname, struct statvfs *statbuf)
+{
+ int fd;
+ int ret;
- struct fuse_context *ctx;
- struct sharefs_private *priv;
- GET_CONTEXT_PRIV(ctx, priv);
- MUST_TRANSLATE_PATHNAME(pathname);
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
- if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_CLOEXEC)) == -1)
- return -errno;
- if ((ret = fstatvfs(fd, statbuf)) == -1)
- ret = -errno;
- close(fd);
- return ret;
+ if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_CLOEXEC)) == -1)
+ return -errno;
+ if ((ret = fstatvfs(fd, statbuf)) == -1)
+ ret = -errno;
+ close(fd);
+ return ret;
}
-int sharefs_release(const char *pathname, struct fuse_file_info *fi) {
- (void)pathname;
+int sharefs_release(const char *pathname, struct fuse_file_info *fi)
+{
+ (void)pathname;
- return close(fi->fh);
+ return close(fi->fh);
}
-int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi) {
- (void)pathname;
+int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi)
+{
+ (void)pathname;
- if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1)
- return -errno;
- return 0;
+ if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1)
+ return -errno;
+ return 0;
}
diff --git a/cmd/sharefs/fuse-operations.h b/cmd/sharefs/fuse-operations.h
index 2150571a..fae181d1 100644
--- a/cmd/sharefs/fuse-operations.h
+++ b/cmd/sharefs/fuse-operations.h
@@ -6,29 +6,43 @@
#error This package requires libfuse >= v3.12
#endif
-#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
-#define SHAREFS_PERM_DIR 0770 /* permission bits for directories presented to userspace */
-#define SHAREFS_PERM_REG 0660 /* permission bits for regular files presented to userspace */
-#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
+/* owning gid presented to userspace */
+#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1
+/* permission bits for directories presented to userspace */
+#define SHAREFS_PERM_DIR 0770
+/* permission bits for regular files presented to userspace */
+#define SHAREFS_PERM_REG 0660
+/* these open flags are cleared unconditionally */
+#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT
/* sharefs_private is populated by sharefs_init and contains process-wide context */
struct sharefs_private {
- int dirfd; /* source dirfd opened during sharefs_init */
- uintptr_t setup; /* cgo handle of opaque setup state */
+ int dirfd; /* source dirfd opened during sharefs_init */
+ uintptr_t setup; /* cgo handle of opaque setup state */
};
-int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi);
-int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags);
+int sharefs_getattr(const char *pathname, struct stat *statbuf,
+ struct fuse_file_info *fi);
+int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler,
+ off_t offset, struct fuse_file_info *fi,
+ enum fuse_readdir_flags flags);
int sharefs_mkdir(const char *pathname, mode_t mode);
int sharefs_unlink(const char *pathname);
int sharefs_rmdir(const char *pathname);
-int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags);
-int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi);
-int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi);
-int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi);
+int sharefs_rename(const char *oldpath, const char *newpath,
+ unsigned int flags);
+int sharefs_truncate(const char *pathname, off_t length,
+ struct fuse_file_info *fi);
+int sharefs_utimens(const char *pathname, const struct timespec times[2],
+ struct fuse_file_info *fi);
+int sharefs_create(const char *pathname, mode_t mode,
+ struct fuse_file_info *fi);
int sharefs_open(const char *pathname, struct fuse_file_info *fi);
-int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
-int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
+int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset,
+ struct fuse_file_info *fi);
+int sharefs_write(const char *pathname, const char *buf, size_t count,
+ off_t offset, struct fuse_file_info *fi);
int sharefs_statfs(const char *pathname, struct statvfs *statbuf);
int sharefs_release(const char *pathname, struct fuse_file_info *fi);
-int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi);
+int sharefs_fsync(const char *pathname, int datasync,
+ struct fuse_file_info *fi);
diff --git a/cmd/sharefs/testsuite/main.go b/cmd/sharefs/testsuite/main.go
new file mode 100644
index 00000000..167875a1
--- /dev/null
+++ b/cmd/sharefs/testsuite/main.go
@@ -0,0 +1,119 @@
+//go:build testsuite
+
+// The sharefs test program checks cli behaviour and exercises the filesystem
+// implemented by cmd/sharefs using fs_mark.
+package main
+
+import (
+ "errors"
+ "log"
+ "os"
+ "os/exec"
+ "strings"
+ "syscall"
+
+ "hakurei.app/internal/testsuite"
+)
+
+// checkBadOpts invokes cmd/sharefs with the specified options and compares
+// the resulting error message.
+func checkBadOpts(cred *syscall.Credential, opts, want string) {
+ var buf strings.Builder
+ buf.Grow(len(want))
+
+ cmd := exec.Command(
+ "sharefs",
+ "-f",
+ "-o", "source=/etc,"+opts,
+ "/mnt",
+ )
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Pdeathsig: syscall.SIGKILL,
+ Credential: cred,
+ }
+ cmd.Stderr = &buf
+ err := cmd.Run()
+ if err == nil {
+ log.Fatalf("opts=%q, unexpected success", opts)
+ }
+ if e, ok := errors.AsType[*exec.ExitError](err); !ok {
+ log.Fatal(err)
+ } else if !e.Exited() {
+ log.Fatal(e)
+ }
+
+ if got := buf.String(); got != want {
+ log.Fatalf("opts=%q\n\t got:%q\n\twant:%q", opts, got, want)
+ }
+}
+
+func main() {
+ go testsuite.ReceiveSignals()
+
+ cred := syscall.Credential{Uid: 1000, Gid: 100}
+ if err := os.Mkdir("result", 0755); err != nil {
+ log.Fatal(err)
+ }
+
+ done := make(chan struct{})
+ go func() {
+ defer close(done)
+
+ testsuite.MustRun(
+ nil, nil,
+ "fs_mark",
+ "-v",
+ "-d", "/sdcard/fs_mark",
+ "-l", "result/fs_mark.log",
+ )
+ }()
+
+ log.Println("checking malformed setuid/setgid representation")
+ checkBadOpts(&cred, "setuid=ff", "sharefs: invalid value for option setuid\n")
+ checkBadOpts(&cred, "setgid=ff", "sharefs: invalid value for option setgid\n")
+
+ log.Println("checking bounds check for setuid/setgid")
+ checkBadOpts(&cred, "setuid=0", "sharefs: invalid value for option setuid\n")
+ checkBadOpts(&cred, "setgid=0", "sharefs: invalid value for option setgid\n")
+ checkBadOpts(&cred, "setuid=-1", "sharefs: invalid value for option setuid\n")
+ checkBadOpts(&cred, "setgid=-1", "sharefs: invalid value for option setgid\n")
+
+ log.Println("checking non-root setuid/setgid")
+ checkBadOpts(&cred, "setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
+ checkBadOpts(&cred, "setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
+ checkBadOpts(&cred, "setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
+ checkBadOpts(&cred, "mkdir", "sharefs: mkdir has no effect when not starting as root\n")
+
+ log.Println("checking root without setuid/setgid")
+ checkBadOpts(nil, "allow_other", "sharefs: setuid and setgid must not be 0\n")
+ checkBadOpts(nil, "setuid=1023", "sharefs: setuid and setgid must not be 0\n")
+ checkBadOpts(nil, "setgid=1023", "sharefs: setuid and setgid must not be 0\n")
+
+ log.Println("verifying mount point")
+ if err := os.Remove("/mnt"); err != nil {
+ log.Fatal(err)
+ }
+
+ log.Println("checking unprivileged mount/unmount")
+ testsuite.MustRun(&cred, nil, "mkdir", "/tmp/sdcard", "/tmp/persistent")
+ testsuite.MustRun(&cred, nil, "sharefs", "-o", "source=/tmp/persistent", "/tmp/sdcard")
+ testsuite.MustRun(&cred, nil, "touch", "/tmp/sdcard/check")
+ testsuite.MustRun(&cred, nil, "umount", "/tmp/sdcard")
+ testsuite.MustRun(&cred, nil, "rm", "/tmp/persistent/check")
+ testsuite.MustRun(&cred, nil, "rmdir", "/tmp/sdcard", "/tmp/persistent")
+
+ log.Println("waiting for fs_mark to complete")
+ <-done
+
+ const backingDir = "/var/lib/sdcard"
+ sharefsCred := syscall.Credential{Uid: 1023, Gid: 1023}
+ log.Println("checking permissions")
+ testsuite.MustRun(&sharefsCred, nil, "touch", backingDir+"/fs_mark/.check")
+ testsuite.MustRun(&sharefsCred, nil, "rm", backingDir+"/fs_mark/.check")
+ testsuite.MustRun(&cred, nil, "rm", "-rf", "/sdcard/fs_mark")
+ if _, err := os.ReadDir(backingDir + "/fs_mark"); err == nil {
+ log.Fatal("fs_mark directory was not removed")
+ } else if !errors.Is(err, os.ErrNotExist) {
+ log.Fatal(err)
+ }
+}
diff --git a/cmd/sharefs/testsuite/raceattr.go b/cmd/sharefs/testsuite/raceattr.go
new file mode 100644
index 00000000..412cb2b3
--- /dev/null
+++ b/cmd/sharefs/testsuite/raceattr.go
@@ -0,0 +1,122 @@
+//go:build raceattr
+
+// The raceattr program reproduces vfs inode file attribute race.
+//
+// Even though libfuse high-level API presents the address of a struct stat
+// alongside struct fuse_context, file attributes are actually inherent to the
+// inode, instead of the specific call from userspace. The kernel implementation
+// in fs/fuse/xattr.c appears to make stale data in the inode (set by a previous
+// call) impossible or very unlikely to reach userspace via the stat family of
+// syscalls. However, when using default_permissions to have the VFS check
+// permissions, this race still happens, despite the resulting struct stat being
+// correct when overriding the check via capabilities otherwise.
+//
+// This program reproduces the failure, but because of its continuous nature, it
+// is provided independent of the vm integration test suite.
+package main
+
+import (
+ "context"
+ "flag"
+ "log"
+ "os"
+ "os/signal"
+ "runtime"
+ "sync"
+ "sync/atomic"
+ "syscall"
+)
+
+func newStatAs(
+ ctx context.Context, cancel context.CancelFunc,
+ n *atomic.Uint64, ok *atomic.Bool,
+ uid uint32, pathname string,
+ continuous bool,
+) func() {
+ return func() {
+ runtime.LockOSThread()
+ defer cancel()
+
+ if _, _, errno := syscall.Syscall(
+ syscall.SYS_SETUID, uintptr(uid),
+ 0, 0,
+ ); errno != 0 {
+ cancel()
+ log.Printf("cannot set uid to %d: %s", uid, errno)
+ }
+
+ var stat syscall.Stat_t
+ for {
+ if ctx.Err() != nil {
+ return
+ }
+
+ if err := syscall.Lstat(pathname, &stat); err != nil {
+ // SHAREFS_PERM_DIR not world executable, or
+ // SHAREFS_PERM_REG not world readable
+ if !continuous {
+ cancel()
+ }
+ ok.Store(true)
+ log.Printf("uid %d: %v", uid, err)
+ } else if stat.Uid != uid {
+ // appears to be unreachable
+ if !continuous {
+ cancel()
+ }
+ ok.Store(true)
+ log.Printf("got uid %d instead of %d", stat.Uid, uid)
+ }
+ n.Add(1)
+ }
+ }
+}
+
+func main() {
+ log.SetFlags(0)
+ log.SetPrefix("raceattr: ")
+
+ p := flag.String("target", "/sdcard/raceattr", "pathname of test file")
+ u0 := flag.Int("uid0", 1<<10-1, "first uid")
+ u1 := flag.Int("uid1", 1<<10-2, "second uid")
+ count := flag.Int("count", 1, "threads per uid")
+ continuous := flag.Bool("continuous", false, "keep running even after reproduce")
+ flag.Parse()
+
+ if os.Geteuid() != 0 {
+ log.Fatal("this program must run as root")
+ }
+
+ ctx, cancel := signal.NotifyContext(
+ context.Background(),
+ syscall.SIGINT,
+ syscall.SIGTERM,
+ syscall.SIGHUP,
+ )
+
+ if err := os.WriteFile(*p, nil, 0); err != nil {
+ log.Fatal(err)
+ }
+
+ var (
+ wg sync.WaitGroup
+
+ n atomic.Uint64
+ ok atomic.Bool
+ )
+
+ if *count < 1 {
+ *count = 1
+ }
+ for range *count {
+ wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u0), *p, *continuous))
+ if *u1 >= 0 {
+ wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u1), *p, *continuous))
+ }
+ }
+
+ wg.Wait()
+ if !*continuous && ok.Load() {
+ log.Printf("reproduced after %d calls", n.Load())
+ }
+}