diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 71cb09e..3870bbb 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 0000000..7725b67 --- /dev/null +++ b/cmd/sharefs/fuse-helper.c @@ -0,0 +1,316 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE /* O_DIRECT */ +#endif + +#include +#include +#include + +/* TODO(ophestra): remove after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */ +#include + +#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 0000000..fdcd1d5 --- /dev/null +++ b/cmd/sharefs/fuse-helper.h @@ -0,0 +1,29 @@ +#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 4) +#include +#include /* 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 0000000..ac276dd --- /dev/null +++ b/cmd/sharefs/fuse.go @@ -0,0 +1,265 @@ +package main + +/* +#cgo pkg-config: --static fuse3 + +#include "fuse-helper.h" +#include +#include + +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] \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 0000000..44a7e51 --- /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 0000000..d9c011b --- /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 0000000..53ab8fc --- /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 0000000..58062e0 --- /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 747f77f..fff1df3 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 d06495d..b5b1abd 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 db3f2c2..15e6f9f 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 63e6fcd..3d53425 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 3c63a3b..ef5d0ad 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" = {