diff options
Diffstat (limited to 'cmd/sharefs')
| -rw-r--r-- | cmd/sharefs/fuse-operations.c | 414 | ||||
| -rw-r--r-- | cmd/sharefs/fuse-operations.h | 44 | ||||
| -rw-r--r-- | cmd/sharefs/testsuite/main.go | 119 | ||||
| -rw-r--r-- | cmd/sharefs/testsuite/raceattr.go | 122 |
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()) + } +} |
