aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitea/workflows/test.yml18
-rw-r--r--cmd/sharefs/fuse-helper.c316
-rw-r--r--cmd/sharefs/fuse-helper.h29
-rw-r--r--cmd/sharefs/fuse.go265
-rw-r--r--cmd/sharefs/main.go31
-rw-r--r--cmd/sharefs/test/configuration.nix39
-rw-r--r--cmd/sharefs/test/default.nix44
-rw-r--r--cmd/sharefs/test/test.py14
-rw-r--r--flake.nix2
-rw-r--r--nixos.nix96
-rw-r--r--options.nix34
-rw-r--r--package.nix4
-rw-r--r--test/interactive/hakurei.nix3
13 files changed, 878 insertions, 17 deletions
diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml
index 71cb09e8..3870bbb4 100644
--- a/.gitea/workflows/test.yml
+++ b/.gitea/workflows/test.yml
@@ -72,6 +72,23 @@ jobs:
path: result/*
retention-days: 1
+ sharefs:
+ name: ShareFS
+ runs-on: nix
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Run NixOS test
+ run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sharefs
+
+ - name: Upload test output
+ uses: actions/upload-artifact@v3
+ with:
+ name: "sharefs-vm-output"
+ path: result/*
+ retention-days: 1
+
hpkg:
name: Hpkg
runs-on: nix
@@ -96,6 +113,7 @@ jobs:
- race
- sandbox
- sandbox-race
+ - sharefs
- hpkg
runs-on: nix
steps:
diff --git a/cmd/sharefs/fuse-helper.c b/cmd/sharefs/fuse-helper.c
new file mode 100644
index 00000000..7725b679
--- /dev/null
+++ b/cmd/sharefs/fuse-helper.c
@@ -0,0 +1,316 @@
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE /* O_DIRECT */
+#endif
+
+#include <dirent.h>
+#include <errno.h>
+#include <unistd.h>
+
+/* TODO(ophestra): remove after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
+#include <sys/syscall.h>
+
+#include "fuse-helper.h"
+#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
+#define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
+#define SHAREFS_PERM_REG 0600 /* permission bits for regular files presented to userspace */
+#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
+
+/* translate_pathname translates a userspace pathname to a relative pathname;
+ * the returned address is a constant string or part of pathname, it is never heap allocated. */
+static inline const char *translate_pathname(const char *pathname) {
+ if (pathname == NULL) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ while (*pathname == '/')
+ pathname++;
+ if (*pathname == '\0')
+ pathname = ".";
+ return pathname;
+}
+
+#define MUST_TRANSLATE_PATHNAME(pathname) \
+ do { \
+ pathname = translate_pathname(pathname); \
+ if (pathname == NULL) \
+ return -errno; \
+ } 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)
+
+/* 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 */
+ }
+
+#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 */
+
+ 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);
+
+ (void)fi;
+
+ if (fstatat(priv->dirfd, pathname, statbuf, AT_SYMLINK_NOFOLLOW) == -1)
+ return -errno;
+ return impl_getattr(ctx, statbuf);
+}
+
+int sharefs_readlink(const char *pathname, char *buf, size_t bufsiz) {
+ int res;
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
+
+ if ((res = readlinkat(priv->dirfd, pathname, buf, bufsiz - 1)) == -1)
+ return -errno;
+ buf[res] = '\0';
+ return 0;
+}
+
+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);
+
+ (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;
+ }
+
+ 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;
+
+ 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;
+ }
+
+ errno = 0; /* for the next readdir call */
+ }
+ if (ret == 0 && errno != 0)
+ ret = -errno; /* readdir */
+
+ 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);
+
+ (void)mode;
+
+ 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);
+
+ 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);
+
+ if (unlinkat(priv->dirfd, pathname, AT_REMOVEDIR) == -1)
+ return -errno;
+ return 0;
+}
+
+int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags) {
+ int res;
+
+ 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;
+}
+
+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);
+
+ (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;
+}
+
+int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi) {
+ int res;
+
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
+
+ (void)fi;
+
+ 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;
+
+ struct fuse_context *ctx;
+ struct sharefs_private *priv;
+ GET_CONTEXT_PRIV(ctx, priv);
+ MUST_TRANSLATE_PATHNAME(pathname);
+
+ (void)mode;
+ (void)fi;
+
+ 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;
+
+ 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;
+}
+
+int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
+ int ret;
+
+ (void)pathname;
+
+ 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;
+
+ (void)pathname;
+
+ 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;
+
+ 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;
+}
+
+int sharefs_release(const char *pathname, struct fuse_file_info *fi) {
+ (void)pathname;
+
+ return close(fi->fh);
+}
+
+int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi) {
+ int res;
+
+ (void)pathname;
+
+ if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1)
+ return -errno;
+ return 0;
+}
diff --git a/cmd/sharefs/fuse-helper.h b/cmd/sharefs/fuse-helper.h
new file mode 100644
index 00000000..fdcd1d5e
--- /dev/null
+++ b/cmd/sharefs/fuse-helper.h
@@ -0,0 +1,29 @@
+#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 4)
+#include <fuse.h>
+#include <fuse_lowlevel.h> /* for fuse_cmdline_help */
+
+#if (FUSE_VERSION < FUSE_MAKE_VERSION(3, 4))
+#error This package requires libfuse >= v3.4
+#endif
+
+/* sharefs_private is populated by sharefs_init and contains process-wide context */
+struct sharefs_private {
+ int dirfd; /* source dirfd opened during sharefs_init */
+};
+
+int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi);
+int sharefs_readlink(const char *pathname, char *buf, size_t bufsiz);
+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_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_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);
diff --git a/cmd/sharefs/fuse.go b/cmd/sharefs/fuse.go
new file mode 100644
index 00000000..ac276dd7
--- /dev/null
+++ b/cmd/sharefs/fuse.go
@@ -0,0 +1,265 @@
+package main
+
+/*
+#cgo pkg-config: --static fuse3
+
+#include "fuse-helper.h"
+#include <stdlib.h>
+#include <string.h>
+
+extern void *sharefs_init(struct fuse_conn_info *conn, struct fuse_config *cfg);
+void sharefs_destroy(void *private_data);
+
+typedef void (*closure)();
+static inline struct fuse_opt _FUSE_OPT_END() { return (struct fuse_opt)FUSE_OPT_END; };
+static inline int _fuse_main(int argc, char *argv[], const struct fuse_operations *op, void *user_data) { return fuse_main(argc, argv, op, user_data); }
+*/
+import "C"
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strconv"
+ "syscall"
+ "unsafe"
+)
+
+type (
+ // closure represents a C function pointer.
+ closure = C.closure
+
+ // fuseArgs represents the fuse_args structure.
+ fuseArgs = C.struct_fuse_args
+)
+
+var (
+ // initFailed is set to true by sharefs_init if initialisation was unsuccessful.
+ initFailed bool
+
+ // initSource is pathname to the writable source directory used by sharefs_init to populate sharefs_private.
+ initSource string
+ // initSetUidGid is the uid and gid to set by sharefs_init when running as root.
+ initSetUidGid [2]int
+)
+
+//export sharefs_init
+func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.Pointer {
+ private_data := C.malloc(C.size_t(unsafe.Sizeof(C.struct_sharefs_private{})))
+ priv := (*C.struct_sharefs_private)(private_data)
+
+ if os.Geteuid() == 0 {
+ if initSetUidGid[0] <= 0 || initSetUidGid[1] <= 0 {
+ log.Println("setuid and setgid must not be 0")
+ goto fail
+ }
+ if err := syscall.Setresgid(initSetUidGid[1], initSetUidGid[1], initSetUidGid[1]); err != nil {
+ log.Printf("cannot set gid: %v", err)
+ goto fail
+ }
+ if err := syscall.Setgroups(nil); err != nil {
+ log.Printf("cannot set supplementary groups: %v", err)
+ goto fail
+ }
+ if err := syscall.Setresuid(initSetUidGid[0], initSetUidGid[0], initSetUidGid[0]); err != nil {
+ log.Printf("cannot set uid: %v", err)
+ goto fail
+ }
+ }
+
+ cfg.use_ino = C.true
+ cfg.direct_io = C.false
+ // getattr is context-dependent
+ cfg.attr_timeout = 0
+ cfg.entry_timeout = 0
+ cfg.negative_timeout = 0
+
+ // all future filesystem operations happen through this dirfd
+ if fd, err := syscall.Open(initSource, syscall.O_DIRECTORY|syscall.O_RDONLY, 0); err != nil {
+ log.Printf("cannot open %q: %v", initSource, err)
+ goto fail
+ } else if err = syscall.Fchdir(fd); err != nil {
+ log.Printf("cannot enter %q: %s", initSource, err)
+ goto fail
+ } else {
+ priv.dirfd = C.int(fd)
+ }
+
+ return private_data
+
+fail:
+ C.free(private_data)
+ C.fuse_exit(C.fuse_get_context().fuse)
+ initFailed = true
+ return nil
+}
+
+//export sharefs_destroy
+func sharefs_destroy(private_data unsafe.Pointer) {
+ if private_data != nil {
+ defer C.free(private_data)
+ priv := (*C.struct_sharefs_private)(private_data)
+
+ if err := syscall.Close(int(priv.dirfd)); err != nil {
+ log.Printf("cannot close source directory: %v", err)
+ }
+ }
+}
+
+// showHelp prints the help message.
+func showHelp() {
+ fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
+ C.fuse_cmdline_help()
+ C.fuse_lowlevel_help()
+ fmt.Println(" -o source=/data/media source directory to be mounted")
+ fmt.Println(" -o setuid=1023 uid to run as when starting as root")
+ fmt.Println(" -o setgid=1023 gid to run as when starting as root")
+}
+
+// parseOpts parses fuse options via fuse_opt_parse.
+func parseOpts(args *C.struct_fuse_args) (
+ source string,
+ setuid, setgid int,
+ ret int,
+) {
+ var unsafeOpts struct {
+ // Pathname to writable source directory.
+ source *C.char
+
+ // Decimal string representation of uid to set when running as root.
+ setuid *C.char
+ // Decimal string representation of gid to set when running as root.
+ setgid *C.char
+ }
+
+ if C.fuse_opt_parse(args, unsafe.Pointer(&unsafeOpts), &[]C.struct_fuse_opt{
+ {templ: C.CString("source=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.source)), value: 0},
+ {templ: C.CString("setuid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setuid)), value: 0},
+ {templ: C.CString("setgid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setgid)), value: 0},
+
+ C._FUSE_OPT_END(),
+ }[0], nil) == -1 {
+ ret = 1
+ return
+ }
+
+ if unsafeOpts.source != nil {
+ defer C.free(unsafe.Pointer(unsafeOpts.source))
+ }
+ if unsafeOpts.setuid != nil {
+ defer C.free(unsafe.Pointer(unsafeOpts.setuid))
+ }
+ if unsafeOpts.setgid != nil {
+ defer C.free(unsafe.Pointer(unsafeOpts.setgid))
+ }
+
+ if unsafeOpts.source == nil {
+ showHelp()
+ ret = 1
+ return
+ } else {
+ source = C.GoString(unsafeOpts.source)
+ }
+
+ if unsafeOpts.setuid == nil {
+ setuid = -1
+ } else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setuid)); err != nil || v <= 0 {
+ log.Println("invalid value for option setuid")
+ ret = 1
+ return
+ } else {
+ setuid = v
+ }
+ if unsafeOpts.setgid == nil {
+ setgid = -1
+ } else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setgid)); err != nil || v <= 0 {
+ log.Println("invalid value for option setgid")
+ ret = 1
+ return
+ } else {
+ setgid = v
+ }
+
+ return
+}
+
+// copyStrings returns a copy of s with null-termination.
+func copyStrings(s ...string) **C.char {
+ if len(s) == 0 {
+ return nil
+ }
+ args := unsafe.Slice((**C.char)(C.malloc(C.size_t(uintptr(len(s))*unsafe.Sizeof(s[0])))), len(s))
+ for i, arg := range s {
+ args[i] = C.CString(arg)
+ }
+ return &args[0]
+}
+
+// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
+// The last byte of arg must be 0.
+func unsafeAddArgument(args *fuseArgs, arg string) {
+ C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
+}
+
+func _main(argc int, argv **C.char) int {
+ args := C.struct_fuse_args{argc: C.int(argc), argv: argv, allocated: 1}
+
+ // this causes the kernel to enforce access control based on
+ // struct stat populated by sharefs_getattr
+ unsafeAddArgument(&args, "-odefault_permissions\x00")
+
+ {
+ source, setuid, setgid, ret := parseOpts(&args)
+ if ret != 0 {
+ return ret
+ }
+
+ if a, err := filepath.Abs(source); err != nil {
+ log.Println(err)
+ return 1
+ } else {
+ initSource = a
+ }
+
+ if os.Geteuid() == 0 {
+ if setuid <= 0 || setgid <= 0 {
+ log.Println("setuid and setgid must not be 0")
+ return 1
+ }
+ } else if setuid > 0 || setgid > 0 {
+ log.Println("setuid and setgid has no effect when not starting as root")
+ return 1
+ }
+ initSetUidGid[0], initSetUidGid[1] = setuid, setgid
+ }
+
+ // TODO(ophestra): spawn container here, set PR_SET_NO_NEW_PRIVS and enforce landlock
+ fuse_main_return := C._fuse_main(args.argc, args.argv, &C.struct_fuse_operations{
+ init: closure(C.sharefs_init),
+ destroy: closure(C.sharefs_destroy),
+
+ // implemented in fuse-helper.c
+ getattr: closure(C.sharefs_getattr),
+ readlink: closure(C.sharefs_readlink),
+ readdir: closure(C.sharefs_readdir),
+ mkdir: closure(C.sharefs_mkdir),
+ unlink: closure(C.sharefs_unlink),
+ rmdir: closure(C.sharefs_rmdir),
+ rename: closure(C.sharefs_rename),
+ truncate: closure(C.sharefs_truncate),
+ utimens: closure(C.sharefs_utimens),
+ create: closure(C.sharefs_create),
+ open: closure(C.sharefs_open),
+ read: closure(C.sharefs_read),
+ write: closure(C.sharefs_write),
+ statfs: closure(C.sharefs_statfs),
+ release: closure(C.sharefs_release),
+ fsync: closure(C.sharefs_fsync),
+ }, nil)
+
+ if initFailed {
+ return 1
+ } else {
+ return int(fuse_main_return)
+ }
+}
diff --git a/cmd/sharefs/main.go b/cmd/sharefs/main.go
new file mode 100644
index 00000000..44a7e510
--- /dev/null
+++ b/cmd/sharefs/main.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+ "log"
+ "os"
+ "path"
+ "runtime"
+ "syscall"
+)
+
+// executableName is the [path.Base] name of the executable that started the current process.
+var executableName = func() string {
+ if len(os.Args) > 0 {
+ return path.Base(os.Args[0])
+ } else if name, err := os.Executable(); err != nil {
+ return "sharefs"
+ } else {
+ return path.Base(name)
+ }
+}()
+
+func main() {
+ runtime.LockOSThread()
+ log.SetFlags(0)
+ log.SetPrefix(executableName + ": ")
+
+ // don't mask creation mode, kernel already did that
+ syscall.Umask(0)
+
+ os.Exit(_main(len(os.Args), copyStrings(os.Args...)))
+}
diff --git a/cmd/sharefs/test/configuration.nix b/cmd/sharefs/test/configuration.nix
new file mode 100644
index 00000000..d9c011bc
--- /dev/null
+++ b/cmd/sharefs/test/configuration.nix
@@ -0,0 +1,39 @@
+{ pkgs, ... }:
+{
+ users.users = {
+ alice = {
+ isNormalUser = true;
+ description = "Alice Foobar";
+ password = "foobar";
+ uid = 1000;
+ };
+ };
+
+ home-manager.users.alice.home.stateVersion = "24.11";
+
+ # Automatically login on tty1 as a normal user:
+ services.getty.autologinUser = "alice";
+
+ # For benchmarking sharefs:
+ environment.systemPackages = [ pkgs.fsmark ];
+
+ virtualisation = {
+ diskSize = 6 * 1024;
+
+ qemu.options = [
+ # Increase test performance:
+ "-smp 8"
+ ];
+ };
+
+ environment.hakurei = rec {
+ enable = true;
+ stateDir = "/var/lib/hakurei";
+ sharefs.source = "${stateDir}/sdcard";
+ users.alice = 0;
+
+ extraHomeConfig = {
+ home.stateVersion = "23.05";
+ };
+ };
+}
diff --git a/cmd/sharefs/test/default.nix b/cmd/sharefs/test/default.nix
new file mode 100644
index 00000000..53ab8fc2
--- /dev/null
+++ b/cmd/sharefs/test/default.nix
@@ -0,0 +1,44 @@
+{
+ testers,
+
+ system,
+ self,
+}:
+testers.nixosTest {
+ name = "sharefs";
+ nodes.machine =
+ { options, pkgs, ... }:
+ let
+ fhs =
+ let
+ hakurei = options.environment.hakurei.package.default;
+ in
+ pkgs.buildFHSEnv {
+ pname = "hakurei-fhs";
+ inherit (hakurei) version;
+ targetPkgs = _: hakurei.targetPkgs;
+ extraOutputsToInstall = [ "dev" ];
+ profile = ''
+ export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
+ '';
+ };
+ in
+ {
+ environment.systemPackages = [
+ # For go tests:
+ (pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" ''
+ cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei"
+ ${fhs}/bin/hakurei-fhs -c 'CC="clang -O3 -Werror" go test ./...'
+ '')
+ ];
+
+ imports = [
+ ./configuration.nix
+
+ self.nixosModules.hakurei
+ self.inputs.home-manager.nixosModules.home-manager
+ ];
+ };
+
+ testScript = builtins.readFile ./test.py;
+}
diff --git a/cmd/sharefs/test/test.py b/cmd/sharefs/test/test.py
new file mode 100644
index 00000000..58062e02
--- /dev/null
+++ b/cmd/sharefs/test/test.py
@@ -0,0 +1,14 @@
+start_all()
+machine.wait_for_unit("multi-user.target")
+
+# Benchmark sharefs:
+machine.succeed("fs_mark -v -d /sdcard/fs_mark -l /tmp/fs_log.txt")
+machine.copy_from_vm("/tmp/fs_log.txt", "")
+
+# Check permissions:
+machine.succeed("ls /var/lib/hakurei/sdcard/fs_mark")
+machine.succeed("sudo -u alice rm -rf /sdcard/fs_mark")
+machine.fail("ls /var/lib/hakurei/sdcard/fs_mark")
+
+# Run hakurei tests on sharefs:
+machine.succeed("sudo -u alice sharefs-workload-hakurei-tests")
diff --git a/flake.nix b/flake.nix
index 747f77f5..fff1df35 100644
--- a/flake.nix
+++ b/flake.nix
@@ -69,6 +69,8 @@
withRace = true;
};
+ sharefs = callPackage ./cmd/sharefs/test { inherit system self; };
+
hpkg = callPackage ./cmd/hpkg/test { inherit system self; };
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
diff --git a/nixos.nix b/nixos.nix
index d06495dc..b5b1abd2 100644
--- a/nixos.nix
+++ b/nixos.nix
@@ -66,6 +66,38 @@ in
) "" cfg.users;
};
+ systemd.services = {
+ sharefs = mkIf (cfg.sharefs.source != null) {
+ unitConfig.RequiresMountsFor = cfg.sharefs.source;
+ serviceConfig = {
+ NoNewPrivileges = true;
+ };
+ script = ''
+ ${pkgs.coreutils}/bin/install \
+ -dm0700 \
+ -o ${cfg.sharefs.user} \
+ -g ${cfg.sharefs.group} \
+ ${cfg.sharefs.source} ${cfg.sharefs.name}
+
+ exec ${cfg.package}/libexec/sharefs -f \
+ -o ${
+ lib.join "," [
+ "noexec"
+ "nosuid"
+ "nodev"
+ "auto_unmount"
+ "allow_other"
+ "clone_fd"
+ "setuid=$(id -u ${cfg.sharefs.user})"
+ "setgid=$(id -g ${cfg.sharefs.group})"
+ "source=${cfg.sharefs.source}"
+ ]
+ } ${cfg.sharefs.name}
+ '';
+ wantedBy = [ "multi-user.target" ];
+ };
+ };
+
home-manager =
let
privPackages = mapAttrs (_: userid: {
@@ -322,25 +354,57 @@ in
in
{
users = mkMerge (
- foldlAttrs (
- acc: _: fid:
- acc
- ++ foldlAttrs (
- acc': _: app:
- acc' ++ [ { ${getsubname fid app.identity} = getuser fid app.identity; } ]
- ) [ { ${getsubname fid 0} = getuser fid 0; } ] cfg.apps
- ) [ ] cfg.users
+ foldlAttrs
+ (
+ acc: _: fid:
+ acc
+ ++ foldlAttrs (
+ acc': _: app:
+ acc' ++ [ { ${getsubname fid app.identity} = getuser fid app.identity; } ]
+ ) [ { ${getsubname fid 0} = getuser fid 0; } ] cfg.apps
+ )
+ (
+ if (cfg.sharefs.source != null) then
+ [
+ {
+ ${cfg.sharefs.user} = {
+ uid = lib.mkDefault 1023;
+ inherit (cfg.sharefs) group;
+ isSystemUser = true;
+ home = cfg.sharefs.source;
+ };
+
+ }
+ ]
+ else
+ [ ]
+ )
+ cfg.users
);
groups = mkMerge (
- foldlAttrs (
- acc: _: fid:
- acc
- ++ foldlAttrs (
- acc': _: app:
- acc' ++ [ { ${getsubname fid app.identity} = getgroup fid app.identity; } ]
- ) [ { ${getsubname fid 0} = getgroup fid 0; } ] cfg.apps
- ) [ ] cfg.users
+ foldlAttrs
+ (
+ acc: _: fid:
+ acc
+ ++ foldlAttrs (
+ acc': _: app:
+ acc' ++ [ { ${getsubname fid app.identity} = getgroup fid app.identity; } ]
+ ) [ { ${getsubname fid 0} = getgroup fid 0; } ] cfg.apps
+ )
+ (
+ if (cfg.sharefs.source != null) then
+ [
+ {
+ ${cfg.sharefs.group} = {
+ gid = lib.mkDefault 1023;
+ };
+ }
+ ]
+ else
+ [ ]
+ )
+ cfg.users
);
};
};
diff --git a/options.nix b/options.nix
index db3f2c21..15e6f9f6 100644
--- a/options.nix
+++ b/options.nix
@@ -40,6 +40,40 @@ in
'';
};
+ sharefs = {
+ user = mkOption {
+ type = types.str;
+ default = "sharefs";
+ description = ''
+ Name of the user to run the sharefs daemon as.
+ '';
+ };
+
+ group = mkOption {
+ type = types.str;
+ default = "sharefs";
+ description = ''
+ Name of the group to run the sharefs daemon as.
+ '';
+ };
+
+ name = mkOption {
+ type = types.str;
+ default = "/sdcard";
+ description = ''
+ Host path to mount sharefs on.
+ '';
+ };
+
+ source = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Writable backing directory. Setting this to null disables sharefs.
+ '';
+ };
+ };
+
apps = mkOption {
type =
let
diff --git a/package.nix b/package.nix
index 63e6fcd0..3d534257 100644
--- a/package.nix
+++ b/package.nix
@@ -13,6 +13,9 @@
wayland-scanner,
xorg,
+ # for sharefs
+ fuse3,
+
# for hpkg
zstd,
gnutar,
@@ -92,6 +95,7 @@ buildGoModule rec {
buildInputs = [
libffi
libseccomp
+ fuse3
acl
wayland
]
diff --git a/test/interactive/hakurei.nix b/test/interactive/hakurei.nix
index 3c63a3bc..ef5d0ada 100644
--- a/test/interactive/hakurei.nix
+++ b/test/interactive/hakurei.nix
@@ -1,8 +1,9 @@
{ pkgs, ... }:
{
- environment.hakurei = {
+ environment.hakurei = rec {
enable = true;
stateDir = "/var/lib/hakurei";
+ sharefs.source = "${stateDir}/sdcard";
users.alice = 0;
apps = {
"cat.gensokyo.extern.foot.noEnablements" = {