Compare commits
53 Commits
v0.3.2
...
wip-bootst
| Author | SHA1 | Date | |
|---|---|---|---|
|
7ad8f15030
|
|||
|
993afde840
|
|||
|
c9cd16fd2a
|
|||
|
e42ea32dbe
|
|||
|
e7982b4ee9
|
|||
|
ef1ebf12d9
|
|||
|
775a9f57c9
|
|||
|
2f8ca83376
|
|||
|
3d720ada92
|
|||
|
2e5362e536
|
|||
|
6d3bd27220
|
|||
|
a27305cb4a
|
|||
|
0e476c5e5b
|
|||
|
54712e0426
|
|||
|
b77c1ecfdb
|
|||
|
dce5839a79
|
|||
|
d597592e1f
|
|||
|
056f5b12d4
|
|||
|
da2bb546ba
|
|||
|
7bfbd59810
|
|||
|
ea815a59e8
|
|||
|
28a8dc67d2
|
|||
|
ec49c63c5f
|
|||
|
5a50bf80ee
|
|||
|
ce06b7b663
|
|||
|
08bdc68f3a
|
|||
|
8cb0b433b2
|
|||
|
767f1844d2
|
|||
|
54610aaddc
|
|||
|
2e80660169
|
|||
|
d0a3c6a2f3
|
|||
|
0c0e3d6fc2
|
|||
|
fae910a1ad
|
|||
|
178c8bc28b
|
|||
|
30dcab0734
|
|||
|
0ea051062b
|
|||
|
b0f2ab6fff
|
|||
|
00a5bdf006
|
|||
|
a27dfdc058
|
|||
|
6d0d9cecd1
|
|||
|
17248d7d61
|
|||
|
41e5628c67
|
|||
|
ffbec828e1
|
|||
|
de0467a65e
|
|||
|
b5999b8814
|
|||
|
ebc67bb8ad
|
|||
|
e60ff660f6
|
|||
|
47db461546
|
|||
|
0a3fe5f907
|
|||
|
b72d502f1c
|
|||
|
f8b3db3f66
|
|||
|
0e2fb1788f
|
|||
|
d8417e2927
|
@@ -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:
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
@@ -187,14 +186,6 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
}})
|
||||
}
|
||||
|
||||
// start pipewire-pulse: this most likely exists on host if PipeWire is available
|
||||
if flagPulse {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSDaemon{
|
||||
Target: fhs.AbsRunUser.Append(strconv.Itoa(container.OverflowUid(msg)), "pulse/native"),
|
||||
Exec: shell, Args: []string{"-lc", "exec pipewire-pulse"},
|
||||
}})
|
||||
}
|
||||
|
||||
config.Container.Filesystem = append(config.Container.Filesystem,
|
||||
// opportunistically bind kvm
|
||||
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
nixosTest,
|
||||
testers,
|
||||
callPackage,
|
||||
|
||||
system,
|
||||
@@ -8,7 +8,7 @@
|
||||
let
|
||||
buildPackage = self.buildPackage.${system};
|
||||
in
|
||||
nixosTest {
|
||||
testers.nixosTest {
|
||||
name = "hpkg";
|
||||
nodes.machine = {
|
||||
environment.etc = {
|
||||
|
||||
282
cmd/sharefs/fuse-operations.c
Normal file
282
cmd/sharefs/fuse-operations.c
Normal file
@@ -0,0 +1,282 @@
|
||||
#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-operations.h"
|
||||
|
||||
/* 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)
|
||||
|
||||
/* 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_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) {
|
||||
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) {
|
||||
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;
|
||||
|
||||
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) {
|
||||
(void)pathname;
|
||||
|
||||
if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
34
cmd/sharefs/fuse-operations.h
Normal file
34
cmd/sharefs/fuse-operations.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12)
|
||||
#include <fuse.h>
|
||||
#include <fuse_lowlevel.h> /* for fuse_cmdline_help */
|
||||
|
||||
#if (FUSE_VERSION < FUSE_MAKE_VERSION(3, 12))
|
||||
#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 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 */
|
||||
|
||||
/* 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 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_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);
|
||||
556
cmd/sharefs/fuse.go
Normal file
556
cmd/sharefs/fuse.go
Normal file
@@ -0,0 +1,556 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo pkg-config: --static fuse3
|
||||
|
||||
#include "fuse-operations.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
extern void *sharefs_init(struct fuse_conn_info *conn, struct fuse_config *cfg);
|
||||
extern void sharefs_destroy(void *private_data);
|
||||
|
||||
typedef void (*closure)();
|
||||
static inline struct fuse_opt _FUSE_OPT_END() { return (struct fuse_opt)FUSE_OPT_END; };
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/helper/proc"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
type (
|
||||
// closure represents a C function pointer.
|
||||
closure = C.closure
|
||||
|
||||
// fuseArgs represents the fuse_args structure.
|
||||
fuseArgs = C.struct_fuse_args
|
||||
|
||||
// setupState holds state used for setup. Its cgo handle is included in
|
||||
// sharefs_private and considered opaque to non-setup callbacks.
|
||||
setupState struct {
|
||||
// Whether sharefs_init failed.
|
||||
initFailed bool
|
||||
|
||||
// Whether to create source directory as root.
|
||||
mkdir bool
|
||||
|
||||
// Open file descriptor to fuse.
|
||||
Fuse int
|
||||
|
||||
// Pathname to open for dirfd.
|
||||
Source *check.Absolute
|
||||
// New uid and gid to set by sharefs_init when starting as root.
|
||||
Setuid, Setgid int
|
||||
}
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(setupState)) }
|
||||
|
||||
// destroySetup invalidates the setup [cgo.Handle] in a sharefs_private structure.
|
||||
func destroySetup(private_data unsafe.Pointer) (ok bool) {
|
||||
if private_data == nil {
|
||||
return false
|
||||
}
|
||||
priv := (*C.struct_sharefs_private)(private_data)
|
||||
|
||||
if h := cgo.Handle(priv.setup); h != 0 {
|
||||
priv.setup = 0
|
||||
h.Delete()
|
||||
ok = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//export sharefs_init
|
||||
func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.Pointer {
|
||||
ctx := C.fuse_get_context()
|
||||
priv := (*C.struct_sharefs_private)(ctx.private_data)
|
||||
setup := cgo.Handle(priv.setup).Value().(*setupState)
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
log.Println("filesystem daemon must not run as root")
|
||||
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(setup.Source.String(), syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC, 0); err != nil {
|
||||
log.Printf("cannot open %q: %v", setup.Source, err)
|
||||
goto fail
|
||||
} else if err = syscall.Fchdir(fd); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
log.Printf("cannot enter %q: %s", setup.Source, err)
|
||||
goto fail
|
||||
} else {
|
||||
priv.dirfd = C.int(fd)
|
||||
}
|
||||
|
||||
return ctx.private_data
|
||||
|
||||
fail:
|
||||
setup.initFailed = true
|
||||
C.fuse_exit(ctx.fuse)
|
||||
return nil
|
||||
}
|
||||
|
||||
//export sharefs_destroy
|
||||
func sharefs_destroy(private_data unsafe.Pointer) {
|
||||
if private_data != nil {
|
||||
destroySetup(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(args *fuseArgs) {
|
||||
executableName := sharefsName
|
||||
if args.argc > 0 {
|
||||
executableName = path.Base(C.GoString(*args.argv))
|
||||
} else if name, err := os.Executable(); err == nil {
|
||||
executableName = path.Base(name)
|
||||
}
|
||||
|
||||
fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
|
||||
|
||||
fmt.Println("Filesystem options:")
|
||||
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")
|
||||
|
||||
fmt.Println("\nFUSE options:")
|
||||
C.fuse_cmdline_help()
|
||||
C.fuse_lib_help(args)
|
||||
}
|
||||
|
||||
// parseOpts parses fuse options via fuse_opt_parse.
|
||||
func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
|
||||
var unsafeOpts struct {
|
||||
// Pathname to writable source directory.
|
||||
source *C.char
|
||||
|
||||
// Whether to create source directory as root.
|
||||
mkdir C.int
|
||||
|
||||
// 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
|
||||
|
||||
// Decimal string representation of open file descriptor to read setupState from.
|
||||
// This is an internal detail for containerisation and must not be specified directly.
|
||||
setup *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("mkdir"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.mkdir)), value: 1},
|
||||
{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},
|
||||
|
||||
{templ: C.CString("setup=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setup)), value: 0},
|
||||
|
||||
C._FUSE_OPT_END(),
|
||||
}[0], nil) == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
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.setup != nil {
|
||||
defer C.free(unsafe.Pointer(unsafeOpts.setup))
|
||||
|
||||
if v, err := strconv.Atoi(C.GoString(unsafeOpts.setup)); err != nil || v < 3 {
|
||||
log.Println("invalid value for option setup")
|
||||
return false
|
||||
} else {
|
||||
r := os.NewFile(uintptr(v), "setup")
|
||||
defer func() {
|
||||
if err = r.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
if err = gob.NewDecoder(r).Decode(setup); err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if setup.Fuse < 3 {
|
||||
log.Println("invalid file descriptor", setup.Fuse)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if unsafeOpts.source == nil {
|
||||
showHelp(args)
|
||||
return false
|
||||
} else if a, err := check.NewAbs(C.GoString(unsafeOpts.source)); err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
} else {
|
||||
setup.Source = a
|
||||
}
|
||||
setup.mkdir = unsafeOpts.mkdir != 0
|
||||
|
||||
if unsafeOpts.setuid == nil {
|
||||
setup.Setuid = -1
|
||||
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setuid)); err != nil || v <= 0 {
|
||||
log.Println("invalid value for option setuid")
|
||||
return false
|
||||
} else {
|
||||
setup.Setuid = v
|
||||
}
|
||||
if unsafeOpts.setgid == nil {
|
||||
setup.Setgid = -1
|
||||
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setgid)); err != nil || v <= 0 {
|
||||
log.Println("invalid value for option setgid")
|
||||
return false
|
||||
} else {
|
||||
setup.Setgid = v
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// copyArgs returns a heap allocated copy of an argument slice in fuse_args representation.
|
||||
func copyArgs(s ...string) fuseArgs {
|
||||
if len(s) == 0 {
|
||||
return fuseArgs{argc: 0, argv: nil, allocated: 0}
|
||||
}
|
||||
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 fuseArgs{argc: C.int(len(s)), argv: &args[0], allocated: 1}
|
||||
}
|
||||
|
||||
// freeArgs frees the contents of argument list.
|
||||
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }
|
||||
|
||||
// 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(s ...string) (exitCode int) {
|
||||
msg := message.New(log.Default())
|
||||
container.TryArgv0(msg)
|
||||
runtime.LockOSThread()
|
||||
|
||||
// don't mask creation mode, kernel already did that
|
||||
syscall.Umask(0)
|
||||
|
||||
var pinner runtime.Pinner
|
||||
defer pinner.Unpin()
|
||||
|
||||
args := copyArgs(s...)
|
||||
defer freeArgs(&args)
|
||||
|
||||
// this causes the kernel to enforce access control based on
|
||||
// struct stat populated by sharefs_getattr
|
||||
unsafeAddArgument(&args, "-odefault_permissions\x00")
|
||||
|
||||
var priv C.struct_sharefs_private
|
||||
pinner.Pin(&priv)
|
||||
var setup setupState
|
||||
priv.setup = C.uintptr_t(cgo.NewHandle(&setup))
|
||||
defer destroySetup(unsafe.Pointer(&priv))
|
||||
|
||||
var opts C.struct_fuse_cmdline_opts
|
||||
if C.fuse_parse_cmdline(&args, &opts) != 0 {
|
||||
return 1
|
||||
}
|
||||
if opts.mountpoint != nil {
|
||||
defer C.free(unsafe.Pointer(opts.mountpoint))
|
||||
}
|
||||
|
||||
if opts.show_version != 0 {
|
||||
fmt.Println("hakurei version", info.Version())
|
||||
fmt.Println("FUSE library version", C.GoString(C.fuse_pkgversion()))
|
||||
C.fuse_lowlevel_version()
|
||||
return 0
|
||||
}
|
||||
|
||||
if opts.show_help != 0 {
|
||||
showHelp(&args)
|
||||
return 0
|
||||
} else if opts.mountpoint == nil {
|
||||
log.Println("no mountpoint specified")
|
||||
return 2
|
||||
} else {
|
||||
// hack to keep fuse_parse_cmdline happy in the container
|
||||
mountpoint := C.GoString(opts.mountpoint)
|
||||
pathnameArg := -1
|
||||
for i, arg := range s {
|
||||
if arg == mountpoint {
|
||||
pathnameArg = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if pathnameArg < 0 {
|
||||
log.Println("mountpoint must be absolute")
|
||||
return 2
|
||||
}
|
||||
s[pathnameArg] = container.Nonexistent
|
||||
}
|
||||
|
||||
if !parseOpts(&args, &setup, msg.GetLogger()) {
|
||||
return 1
|
||||
}
|
||||
asRoot := os.Geteuid() == 0
|
||||
|
||||
if asRoot {
|
||||
if setup.Setuid <= 0 || setup.Setgid <= 0 {
|
||||
log.Println("setuid and setgid must not be 0")
|
||||
return 1
|
||||
}
|
||||
|
||||
if setup.Fuse >= 3 {
|
||||
log.Println("filesystem daemon must not run as root")
|
||||
return 1
|
||||
}
|
||||
|
||||
if setup.mkdir {
|
||||
if err := os.MkdirAll(setup.Source.String(), 0700); err != nil {
|
||||
if !errors.Is(err, os.ErrExist) {
|
||||
log.Println(err)
|
||||
return 1
|
||||
}
|
||||
// skip setup for existing source directory
|
||||
} else if err = os.Chown(setup.Source.String(), setup.Setuid, setup.Setgid); err != nil {
|
||||
log.Println(err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
} else if setup.Fuse < 3 && (setup.Setuid > 0 || setup.Setgid > 0) {
|
||||
log.Println("setuid and setgid has no effect when not starting as root")
|
||||
return 1
|
||||
} else if setup.mkdir {
|
||||
log.Println("mkdir has no effect when not starting as root")
|
||||
return 1
|
||||
}
|
||||
|
||||
op := C.struct_fuse_operations{
|
||||
init: closure(C.sharefs_init),
|
||||
destroy: closure(C.sharefs_destroy),
|
||||
|
||||
// implemented in fuse-helper.c
|
||||
getattr: closure(C.sharefs_getattr),
|
||||
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),
|
||||
}
|
||||
|
||||
fuse := C.fuse_new_fn(&args, &op, C.size_t(unsafe.Sizeof(op)), unsafe.Pointer(&priv))
|
||||
if fuse == nil {
|
||||
return 3
|
||||
}
|
||||
defer C.fuse_destroy(fuse)
|
||||
se := C.fuse_get_session(fuse)
|
||||
|
||||
if setup.Fuse < 3 {
|
||||
// unconfined, set up mount point and container
|
||||
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
|
||||
return 4
|
||||
}
|
||||
// unmounted by initial process
|
||||
defer func() {
|
||||
if exitCode == 5 {
|
||||
C.fuse_unmount(fuse)
|
||||
}
|
||||
}()
|
||||
|
||||
if asRoot {
|
||||
if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); err != nil {
|
||||
log.Printf("cannot set gid: %v", err)
|
||||
return 5
|
||||
}
|
||||
if err := syscall.Setgroups(nil); err != nil {
|
||||
log.Printf("cannot set supplementary groups: %v", err)
|
||||
return 5
|
||||
}
|
||||
if err := syscall.Setresuid(setup.Setuid, setup.Setuid, setup.Setuid); err != nil {
|
||||
log.Printf("cannot set uid: %v", err)
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
msg.SwapVerbose(opts.debug != 0)
|
||||
ctx := context.Background()
|
||||
if opts.foreground != 0 {
|
||||
c, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
ctx = c
|
||||
}
|
||||
z := container.New(ctx, msg)
|
||||
z.AllowOrphan = opts.foreground == 0
|
||||
z.Env = os.Environ()
|
||||
|
||||
// keep fuse_parse_cmdline happy in the container
|
||||
z.Tmpfs(check.MustAbs(container.Nonexistent), 1<<10, 0755)
|
||||
|
||||
if a, err := check.NewAbs(container.MustExecutable(msg)); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else {
|
||||
z.Path = a
|
||||
}
|
||||
z.Args = s
|
||||
z.ForwardCancel = true
|
||||
z.SeccompPresets |= std.PresetStrict
|
||||
z.ParentPerm = 0700
|
||||
z.Bind(setup.Source, setup.Source, std.BindWritable)
|
||||
if !z.AllowOrphan {
|
||||
z.WaitDelay = hst.WaitDelayMax
|
||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
}
|
||||
z.Bind(z.Path, z.Path, 0)
|
||||
setup.Fuse = int(proc.ExtraFileSlice(&z.ExtraFiles, os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse")))
|
||||
|
||||
var setupWriter io.WriteCloser
|
||||
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else {
|
||||
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
|
||||
setupWriter = w
|
||||
}
|
||||
|
||||
if err := z.Start(); err != nil {
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
log.Println(m)
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
return 5
|
||||
}
|
||||
if err := z.Serve(); err != nil {
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
log.Println(m)
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
return 5
|
||||
}
|
||||
|
||||
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else if err = setupWriter.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
if !z.AllowOrphan {
|
||||
if err := z.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) || exitError == nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
}
|
||||
switch code := exitError.ExitCode(); syscall.Signal(code & 0x7f) {
|
||||
case syscall.SIGINT:
|
||||
case syscall.SIGTERM:
|
||||
|
||||
default:
|
||||
return code
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
} else { // confined
|
||||
C.free(unsafe.Pointer(opts.mountpoint))
|
||||
// must be heap allocated
|
||||
opts.mountpoint = C.CString("/dev/fd/" + strconv.Itoa(setup.Fuse))
|
||||
|
||||
if err := os.Chdir("/"); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
|
||||
return 4
|
||||
}
|
||||
defer C.fuse_unmount(fuse)
|
||||
|
||||
if C.fuse_set_signal_handlers(se) != 0 {
|
||||
return 6
|
||||
}
|
||||
defer C.fuse_remove_signal_handlers(se)
|
||||
|
||||
if opts.singlethread != 0 {
|
||||
if C.fuse_loop(fuse) != 0 {
|
||||
return 8
|
||||
}
|
||||
} else {
|
||||
loopConfig := C.fuse_loop_cfg_create()
|
||||
if loopConfig == nil {
|
||||
return 7
|
||||
}
|
||||
defer C.fuse_loop_cfg_destroy(loopConfig)
|
||||
|
||||
C.fuse_loop_cfg_set_clone_fd(loopConfig, C.uint(opts.clone_fd))
|
||||
|
||||
C.fuse_loop_cfg_set_idle_threads(loopConfig, opts.max_idle_threads)
|
||||
C.fuse_loop_cfg_set_max_threads(loopConfig, opts.max_threads)
|
||||
if C.fuse_loop_mt(fuse, loopConfig) != 0 {
|
||||
return 8
|
||||
}
|
||||
}
|
||||
|
||||
if setup.initFailed {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
113
cmd/sharefs/fuse_test.go
Normal file
113
cmd/sharefs/fuse_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
)
|
||||
|
||||
func TestParseOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want setupState
|
||||
wantLog string
|
||||
wantOk bool
|
||||
}{
|
||||
{"zero length", []string{}, setupState{}, "", false},
|
||||
|
||||
{"not absolute", []string{"sharefs",
|
||||
"-o", "source=nonexistent",
|
||||
"-o", "setuid=1023",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{}, "sharefs: path \"nonexistent\" is not absolute\n", false},
|
||||
|
||||
{"not specified", []string{"sharefs",
|
||||
"-o", "setuid=1023",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{}, "", false},
|
||||
|
||||
{"invalid setuid", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setuid=ff",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
}, "sharefs: invalid value for option setuid\n", false},
|
||||
|
||||
{"invalid setgid", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setuid=1023",
|
||||
"-o", "setgid=ff",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: 1023,
|
||||
}, "sharefs: invalid value for option setgid\n", false},
|
||||
|
||||
{"simple", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: -1,
|
||||
Setgid: -1,
|
||||
}, "", true},
|
||||
|
||||
{"root", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setuid=1023",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: 1023,
|
||||
Setgid: 1023,
|
||||
}, "", true},
|
||||
|
||||
{"setuid", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setuid=1023",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: 1023,
|
||||
Setgid: -1,
|
||||
}, "", true},
|
||||
|
||||
{"setgid", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: -1,
|
||||
Setgid: 1023,
|
||||
}, "", true},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
got setupState
|
||||
buf bytes.Buffer
|
||||
)
|
||||
args := copyArgs(tc.args...)
|
||||
defer freeArgs(&args)
|
||||
unsafeAddArgument(&args, "-odefault_permissions\x00")
|
||||
|
||||
if ok := parseOpts(&args, &got, log.New(&buf, "sharefs: ", 0)); ok != tc.wantOk {
|
||||
t.Errorf("parseOpts: ok = %v, want %v", ok, tc.wantOk)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(&got, &tc.want) {
|
||||
t.Errorf("parseOpts: setup = %#v, want %#v", got, tc.want)
|
||||
}
|
||||
|
||||
if buf.String() != tc.wantLog {
|
||||
t.Errorf("parseOpts: log =\n%s\nwant\n%s", buf.String(), tc.wantLog)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
31
cmd/sharefs/main.go
Normal file
31
cmd/sharefs/main.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// sharefsName is the prefix used by log.std in the sharefs process.
|
||||
const sharefsName = "sharefs"
|
||||
|
||||
// handleMountArgs returns an alternative, libfuse-compatible args slice for
|
||||
// args passed by mount -t fuse.sharefs [options] sharefs <mountpoint>.
|
||||
//
|
||||
// In this case, args always has a length of 5 with index 0 being what comes
|
||||
// after "fuse." in the filesystem type, 1 is the uninterpreted string passed
|
||||
// to mount (sharefsName is used as the magic string to enable this hack),
|
||||
// 2 is passed through to libfuse as mountpoint, and 3 is always "-o".
|
||||
func handleMountArgs(args []string) []string {
|
||||
if len(args) == 5 && args[1] == sharefsName && args[3] == "-o" {
|
||||
return []string{sharefsName, args[2], "-o", args[4]}
|
||||
}
|
||||
return slices.Clone(args)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix(sharefsName + ": ")
|
||||
|
||||
os.Exit(_main(handleMountArgs(os.Args)...))
|
||||
}
|
||||
29
cmd/sharefs/main_test.go
Normal file
29
cmd/sharefs/main_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandleMountArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{"nil", nil, nil},
|
||||
{"passthrough", []string{"sharefs", "-V"}, []string{"sharefs", "-V"}},
|
||||
{"replace", []string{"/sbin/sharefs", "sharefs", "/sdcard", "-o", "rw"}, []string{"sharefs", "/sdcard", "-o", "rw"}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := handleMountArgs(tc.args); !slices.Equal(got, tc.want) {
|
||||
t.Errorf("handleMountArgs: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
41
cmd/sharefs/test/configuration.nix
Normal file
41
cmd/sharefs/test/configuration.nix
Normal file
@@ -0,0 +1,41 @@
|
||||
{ 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";
|
||||
|
||||
environment = {
|
||||
# For benchmarking sharefs:
|
||||
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";
|
||||
};
|
||||
};
|
||||
}
|
||||
44
cmd/sharefs/test/default.nix
Normal file
44
cmd/sharefs/test/default.nix
Normal file
@@ -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;
|
||||
}
|
||||
60
cmd/sharefs/test/test.py
Normal file
60
cmd/sharefs/test/test.py
Normal file
@@ -0,0 +1,60 @@
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# To check sharefs version:
|
||||
print(machine.succeed("sharefs -V"))
|
||||
|
||||
# Make sure sharefs started:
|
||||
machine.wait_for_unit("sdcard.mount")
|
||||
|
||||
machine.succeed("mkdir /mnt")
|
||||
def check_bad_opts_output(opts, want, source="/etc", privileged=False):
|
||||
output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"sharefs -f -o source={source},{opts} /mnt 2>&1")
|
||||
if output != want:
|
||||
raise Exception(f"unexpected output: {output}")
|
||||
|
||||
# Malformed setuid/setgid representation:
|
||||
check_bad_opts_output("setuid=ff", "sharefs: invalid value for option setuid\n")
|
||||
check_bad_opts_output("setgid=ff", "sharefs: invalid value for option setgid\n")
|
||||
|
||||
# Bounds check for setuid/setgid:
|
||||
check_bad_opts_output("setuid=0", "sharefs: invalid value for option setuid\n")
|
||||
check_bad_opts_output("setgid=0", "sharefs: invalid value for option setgid\n")
|
||||
check_bad_opts_output("setuid=-1", "sharefs: invalid value for option setuid\n")
|
||||
check_bad_opts_output("setgid=-1", "sharefs: invalid value for option setgid\n")
|
||||
|
||||
# Non-root setuid/setgid:
|
||||
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||
check_bad_opts_output("setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||
check_bad_opts_output("mkdir", "sharefs: mkdir has no effect when not starting as root\n")
|
||||
|
||||
# Starting as root without setuid/setgid:
|
||||
check_bad_opts_output("allow_other", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||
|
||||
# Make sure nothing actually got mounted:
|
||||
machine.fail("umount /mnt")
|
||||
machine.succeed("rmdir /mnt")
|
||||
|
||||
# Unprivileged mount/unmount:
|
||||
machine.succeed("sudo -u alice -i mkdir /home/alice/{sdcard,persistent}")
|
||||
machine.succeed("sudo -u alice -i sharefs -o source=/home/alice/persistent /home/alice/sdcard")
|
||||
machine.succeed("sudo -u alice -i touch /home/alice/sdcard/check")
|
||||
machine.succeed("sudo -u alice -i umount /home/alice/sdcard")
|
||||
machine.succeed("sudo -u alice -i rm /home/alice/persistent/check")
|
||||
machine.succeed("sudo -u alice -i rmdir /home/alice/{sdcard,persistent}")
|
||||
|
||||
# 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("sudo -u sharefs touch /var/lib/hakurei/sdcard/fs_mark/.check")
|
||||
machine.succeed("sudo -u sharefs rm /var/lib/hakurei/sdcard/fs_mark/.check")
|
||||
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 -i sharefs-workload-hakurei-tests")
|
||||
@@ -202,7 +202,7 @@ func TestIsAutoRootBindable(t *testing.T) {
|
||||
t.Parallel()
|
||||
var msg message.Msg
|
||||
if tc.log {
|
||||
msg = &kstub{nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
|
||||
msg = &kstub{nil, nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),
|
||||
}})}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ type (
|
||||
// Container represents a container environment being prepared or run.
|
||||
// None of [Container] methods are safe for concurrent use.
|
||||
Container struct {
|
||||
// Whether the container init should stay alive after its parent terminates.
|
||||
AllowOrphan bool
|
||||
// Cgroup fd, nil to disable.
|
||||
Cgroup *int
|
||||
// ExtraFiles passed through to initial process in the container,
|
||||
@@ -252,8 +254,7 @@ func (p *Container) Start() error {
|
||||
}
|
||||
p.cmd.Dir = fhs.Root
|
||||
p.cmd.SysProcAttr = &SysProcAttr{
|
||||
Setsid: !p.RetainSession,
|
||||
Pdeathsig: SIGKILL,
|
||||
Setsid: !p.RetainSession,
|
||||
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
|
||||
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
|
||||
|
||||
@@ -268,6 +269,9 @@ func (p *Container) Start() error {
|
||||
|
||||
UseCgroupFD: p.Cgroup != nil,
|
||||
}
|
||||
if !p.AllowOrphan {
|
||||
p.cmd.SysProcAttr.Pdeathsig = SIGKILL
|
||||
}
|
||||
if p.cmd.SysProcAttr.UseCgroupFD {
|
||||
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
|
||||
}
|
||||
|
||||
@@ -162,7 +162,8 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
|
||||
t.Parallel()
|
||||
|
||||
wait4signal := make(chan struct{})
|
||||
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
|
||||
lockNotify := make(chan struct{})
|
||||
k := &kstub{wait4signal, lockNotify, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, lockNotify, s} }, tc.want)}
|
||||
defer stub.HandleExit(t)
|
||||
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
|
||||
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
|
||||
@@ -200,8 +201,8 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
|
||||
k := &kstub{nil, stub.New(t,
|
||||
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
|
||||
k := &kstub{nil, nil, stub.New(t,
|
||||
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, nil, s} },
|
||||
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
|
||||
)}
|
||||
state := &setupState{Params: tc.params, Msg: k}
|
||||
@@ -322,12 +323,19 @@ const (
|
||||
|
||||
type kstub struct {
|
||||
wait4signal chan struct{}
|
||||
lockNotify chan struct{}
|
||||
*stub.Stub[syscallDispatcher]
|
||||
}
|
||||
|
||||
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
|
||||
|
||||
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") }
|
||||
func (k *kstub) lockOSThread() {
|
||||
k.Helper()
|
||||
expect := k.Expects("lockOSThread")
|
||||
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
|
||||
<-k.lockNotify
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kstub) setPtracer(pid uintptr) error {
|
||||
k.Helper()
|
||||
@@ -472,6 +480,10 @@ func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
|
||||
k.FailNow()
|
||||
}
|
||||
|
||||
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
|
||||
defer close(k.lockNotify)
|
||||
}
|
||||
|
||||
// export channel for external instrumentation
|
||||
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
|
||||
chanf(c)
|
||||
|
||||
@@ -1992,7 +1992,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
|
||||
@@ -2075,7 +2075,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(10)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"forwarding context cancellation"}}, nil, nil),
|
||||
// magicWait4Signal as ret causes wait4 stub to unblock
|
||||
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", os.Interrupt}, magicWait4Signal, stub.UniqueError(9)),
|
||||
@@ -2090,7 +2090,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
|
||||
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
|
||||
@@ -2175,7 +2175,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbosef", stub.ExpectArgs{"got %s, forwarding to initial process", []any{"quit"}}, nil, nil),
|
||||
// magicWait4Signal as ret causes wait4 stub to unblock
|
||||
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", syscall.SIGQUIT}, magicWait4Signal, stub.UniqueError(0xfe)),
|
||||
@@ -2190,7 +2190,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
|
||||
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
|
||||
@@ -2275,7 +2275,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil),
|
||||
call("beforeExit", stub.ExpectArgs{}, nil, nil),
|
||||
call("exit", stub.ExpectArgs{0}, nil, nil),
|
||||
@@ -2283,7 +2283,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
|
||||
@@ -2366,7 +2366,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(5)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
|
||||
@@ -2377,7 +2377,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil}, 0xbad, nil),
|
||||
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
|
||||
@@ -2461,7 +2461,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(3)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
|
||||
@@ -2471,7 +2471,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
@@ -2599,7 +2599,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"initial process exited with code %d", []any{1}}, nil, nil),
|
||||
@@ -2609,7 +2609,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
@@ -2741,7 +2741,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(0)}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/bin/zsh")}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/bin/zsh", []string{"zsh", "-c", "exec vim"}, []string{"DISPLAY=:0"}, "/.hakurei"}, &os.Process{Pid: 0xcafe}, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
|
||||
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
|
||||
@@ -2752,7 +2752,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
|
||||
/* wait4 */
|
||||
Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
|
||||
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
|
||||
|
||||
2
dist/install.sh
vendored
2
dist/install.sh
vendored
@@ -2,7 +2,7 @@
|
||||
cd "$(dirname -- "$0")" || exit 1
|
||||
|
||||
install -vDm0755 "bin/hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hakurei"
|
||||
install -vDm0755 "bin/hpkg" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hpkg"
|
||||
install -vDm0755 "bin/sharefs" "${HAKUREI_INSTALL_PREFIX}/usr/bin/sharefs"
|
||||
|
||||
install -vDm4511 "bin/hsu" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hsu"
|
||||
if [ ! -f "${HAKUREI_INSTALL_PREFIX}/etc/hsurc" ]; then
|
||||
|
||||
16
flake.lock
generated
16
flake.lock
generated
@@ -7,32 +7,32 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756679287,
|
||||
"narHash": "sha256-Xd1vOeY9ccDf5VtVK12yM0FS6qqvfUop8UQlxEB+gTQ=",
|
||||
"lastModified": 1765384171,
|
||||
"narHash": "sha256-FuFtkJrW1Z7u+3lhzPRau69E0CNjADku1mLQQflUORo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "07fc025fe10487dd80f2ec694f1cd790e752d0e8",
|
||||
"rev": "44777152652bc9eacf8876976fa72cc77ca8b9d8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "release-25.05",
|
||||
"ref": "release-25.11",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1757020766,
|
||||
"narHash": "sha256-PLoSjHRa2bUbi1x9HoXgTx2AiuzNXs54c8omhadyvp0=",
|
||||
"lastModified": 1765311797,
|
||||
"narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "fe83bbdde2ccdc2cb9573aa846abe8363f79a97a",
|
||||
"rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.05",
|
||||
"ref": "nixos-25.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
19
flake.nix
19
flake.nix
@@ -2,10 +2,10 @@
|
||||
description = "hakurei container tool and nixos module";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager/release-25.05";
|
||||
url = "github:nix-community/home-manager/release-25.11";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
@@ -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 ]; } ''
|
||||
@@ -136,6 +138,10 @@
|
||||
;
|
||||
};
|
||||
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
|
||||
sharefs = pkgs.linkFarm "sharefs" {
|
||||
"bin/sharefs" = "${hakurei}/libexec/sharefs";
|
||||
"bin/mount.fuse.sharefs" = "${hakurei}/libexec/sharefs";
|
||||
};
|
||||
|
||||
dist = pkgs.runCommand "${hakurei.name}-dist" { buildInputs = hakurei.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
|
||||
# go requires XDG_CACHE_HOME for the build cache
|
||||
@@ -160,7 +166,10 @@
|
||||
pkgs = nixpkgsFor.${system};
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell { buildInputs = hakurei.targetPkgs; };
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = hakurei.targetPkgs;
|
||||
hardeningDisable = [ "fortify" ];
|
||||
};
|
||||
withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
|
||||
|
||||
vm =
|
||||
@@ -185,13 +194,13 @@
|
||||
hakurei =
|
||||
let
|
||||
# this is used for interactive vm testing during development, where tests might be broken
|
||||
package = self.packages.${pkgs.system}.hakurei.override {
|
||||
package = self.packages.${pkgs.stdenv.hostPlatform.system}.hakurei.override {
|
||||
buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; });
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit package;
|
||||
hsuPackage = self.packages.${pkgs.system}.hsu.override { hakurei = package; };
|
||||
hsuPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.hsu.override { hakurei = package; };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,12 +30,46 @@ type Config struct {
|
||||
// This option is unsupported and most likely enables full control over the Wayland
|
||||
// session. Do not set this to true unless you are sure you know what you are doing.
|
||||
DirectWayland bool `json:"direct_wayland,omitempty"`
|
||||
// Direct access to the PipeWire socket established via SecurityContext::Create, no
|
||||
// attempt is made to start the pipewire-pulse server.
|
||||
//
|
||||
// The SecurityContext machinery is fatally flawed, it blindly sets read and execute
|
||||
// bits on all objects for clients with the lowest achievable privilege level (by
|
||||
// setting PW_KEY_ACCESS to "restricted"). This enables them to call any method
|
||||
// targeting any object, and since Registry::Destroy checks for the read and execute bit,
|
||||
// allows the destruction of any object other than PW_ID_CORE as well. This behaviour
|
||||
// is implemented separately in media-session and wireplumber, with the wireplumber
|
||||
// implementation in Lua via an embedded Lua vm. In all known setups, wireplumber is
|
||||
// in use, and there is no known way to change its behaviour and set permissions
|
||||
// differently without replacing the Lua script. Also, since PipeWire relies on these
|
||||
// permissions to work, reducing them is not possible.
|
||||
//
|
||||
// Currently, the only other sandboxed use case is flatpak, which is not aware of
|
||||
// PipeWire and blindly exposes the bare PulseAudio socket to the container (behaves
|
||||
// like DirectPulse). This socket is backed by the pipewire-pulse compatibility daemon,
|
||||
// which obtains client pid via the SO_PEERCRED option. The PipeWire daemon, pipewire-pulse
|
||||
// daemon and the session manager daemon then separately performs the /.flatpak-info hack
|
||||
// described in https://git.gensokyo.uk/security/hakurei/issues/21. Under such use case,
|
||||
// since the client has no direct access to PipeWire, insecure parts of the protocol are
|
||||
// obscured by pipewire-pulse simply not implementing them, and thus hiding the flaws
|
||||
// described above.
|
||||
//
|
||||
// Hakurei does not rely on the /.flatpak-info hack. Instead, a socket is sets up via
|
||||
// SecurityContext. A pipewire-pulse server connected through it achieves the same
|
||||
// permissions as flatpak does via the /.flatpak-info hack and is maintained for the
|
||||
// life of the container.
|
||||
//
|
||||
// This option is unsupported and enables a denial-of-service attack as the sandboxed
|
||||
// client is able to destroy any client object and thus disconnecting them from PipeWire,
|
||||
// or destroy the SecurityContext object preventing any further container creation.
|
||||
// Do not set this to true, it is insecure under any configuration.
|
||||
DirectPipeWire bool `json:"direct_pipewire,omitempty"`
|
||||
// Direct access to PulseAudio socket, no attempt is made to establish pipewire-pulse
|
||||
// server via a PipeWire socket with a SecurityContext attached and the bare socket
|
||||
// is made available to the container.
|
||||
//
|
||||
// This option is unsupported and enables arbitrary code execution as the PulseAudio
|
||||
// server. Do not set this to true, this is insecure under any configuration.
|
||||
// server. Do not set this to true, it is insecure under any configuration.
|
||||
DirectPulse bool `json:"direct_pulse,omitempty"`
|
||||
|
||||
// Extra acl updates to perform before setuid.
|
||||
|
||||
@@ -53,6 +53,10 @@ type syscallDispatcher interface {
|
||||
readdir(name string) ([]os.DirEntry, error)
|
||||
// tempdir provides [os.TempDir].
|
||||
tempdir() string
|
||||
// mkdir provides [os.Mkdir].
|
||||
mkdir(name string, perm os.FileMode) error
|
||||
// removeAll provides [os.RemoveAll].
|
||||
removeAll(path string) error
|
||||
// exit provides [os.Exit].
|
||||
exit(code int)
|
||||
|
||||
@@ -62,6 +66,8 @@ type syscallDispatcher interface {
|
||||
// lookupGroupId calls [user.LookupGroup] and returns the Gid field of the resulting [user.Group] struct.
|
||||
lookupGroupId(name string) (string, error)
|
||||
|
||||
// lookPath provides exec.LookPath.
|
||||
lookPath(file string) (string, error)
|
||||
// cmdOutput provides the Output method of [exec.Cmd].
|
||||
cmdOutput(cmd *exec.Cmd) ([]byte, error)
|
||||
|
||||
@@ -121,6 +127,8 @@ func (direct) stat(name string) (os.FileInfo, error) { return os.Stat(name)
|
||||
func (direct) open(name string) (osFile, error) { return os.Open(name) }
|
||||
func (direct) readdir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
|
||||
func (direct) tempdir() string { return os.TempDir() }
|
||||
func (direct) mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) }
|
||||
func (direct) removeAll(path string) error { return os.RemoveAll(path) }
|
||||
func (direct) exit(code int) { os.Exit(code) }
|
||||
|
||||
func (direct) evalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) }
|
||||
@@ -134,6 +142,7 @@ func (direct) lookupGroupId(name string) (gid string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (direct) lookPath(file string) (string, error) { return exec.LookPath(file) }
|
||||
func (direct) cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() }
|
||||
|
||||
func (direct) notifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {
|
||||
|
||||
@@ -701,10 +701,13 @@ func (panicDispatcher) stat(string) (os.FileInfo, error) { pa
|
||||
func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") }
|
||||
func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
|
||||
func (panicDispatcher) tempdir() string { panic("unreachable") }
|
||||
func (panicDispatcher) mkdir(string, os.FileMode) error { panic("unreachable") }
|
||||
func (panicDispatcher) removeAll(string) error { panic("unreachable") }
|
||||
func (panicDispatcher) exit(int) { panic("unreachable") }
|
||||
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) prctl(uintptr, uintptr, uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) lookPath(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
|
||||
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
|
||||
|
||||
@@ -70,7 +70,7 @@ type outcomeState struct {
|
||||
// Copied from their respective exported values.
|
||||
mapuid, mapgid *stringPair[int]
|
||||
|
||||
// Copied from [EnvPaths] per-process.
|
||||
// Copied from [env.Paths] per-process.
|
||||
sc hst.Paths
|
||||
*env.Paths
|
||||
|
||||
@@ -172,6 +172,8 @@ type outcomeStateSys struct {
|
||||
|
||||
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
|
||||
directWayland bool
|
||||
// Copied from [hst.Config]. Safe for read by spPipeWireOp.toSystem only.
|
||||
directPipeWire bool
|
||||
// Copied from [hst.Config]. Safe for read by spPulseOp.toSystem only.
|
||||
directPulse bool
|
||||
// Copied header from [hst.Config]. Safe for read by spFilesystemOp.toSystem only.
|
||||
@@ -187,9 +189,8 @@ type outcomeStateSys struct {
|
||||
func (s *outcomeState) newSys(config *hst.Config, sys *system.I) *outcomeStateSys {
|
||||
return &outcomeStateSys{
|
||||
appId: config.ID, et: config.Enablements.Unwrap(),
|
||||
directWayland: config.DirectWayland, directPulse: config.DirectPulse,
|
||||
extraPerms: config.ExtraPerms,
|
||||
sessionBus: config.SessionBus, systemBus: config.SystemBus,
|
||||
directWayland: config.DirectWayland, directPipeWire: config.DirectPipeWire, directPulse: config.DirectPulse,
|
||||
extraPerms: config.ExtraPerms, sessionBus: config.SessionBus, systemBus: config.SystemBus,
|
||||
sys: sys, outcomeState: s,
|
||||
}
|
||||
}
|
||||
@@ -256,6 +257,10 @@ type outcomeStateParams struct {
|
||||
// Populated by spRuntimeOp.
|
||||
runtimeDir *check.Absolute
|
||||
|
||||
// Path to pipewire-pulse server.
|
||||
// Populated by spPipeWireOp if DirectPipeWire is false.
|
||||
pipewirePulsePath *check.Absolute
|
||||
|
||||
as hst.ApplyState
|
||||
*outcomeState
|
||||
}
|
||||
@@ -295,7 +300,7 @@ func (state *outcomeStateSys) toSystem() error {
|
||||
// optional via enablements
|
||||
&spWaylandOp{},
|
||||
&spX11Op{},
|
||||
spPipeWireOp{},
|
||||
&spPipeWireOp{},
|
||||
&spPulseOp{},
|
||||
&spDBusOp{},
|
||||
|
||||
|
||||
@@ -68,7 +68,11 @@ func TestOutcomeRun(t *testing.T) {
|
||||
).
|
||||
|
||||
// spPipeWireOp
|
||||
PipeWire(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire")).
|
||||
PipeWire(
|
||||
m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
).
|
||||
|
||||
// spDBusOp
|
||||
MustProxyDBus(
|
||||
@@ -96,7 +100,6 @@ func TestOutcomeRun(t *testing.T) {
|
||||
"GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
|
||||
"HOME=/data/data/org.chromium.Chromium",
|
||||
"PIPEWIRE_REMOTE=/run/user/1971/pipewire-0",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
@@ -146,9 +149,6 @@ func TestOutcomeRun(t *testing.T) {
|
||||
// spWaylandOp
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1971/wayland-0"), 0).
|
||||
|
||||
// spPipeWireOp
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire"), m("/run/user/1971/pipewire-0"), 0).
|
||||
|
||||
// spDBusOp
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1971/bus"), 0).
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
|
||||
@@ -170,7 +170,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
Remount(fhs.AbsRoot, syscall.MS_RDONLY),
|
||||
}},
|
||||
|
||||
{"nixos permissive defaults no enablements", new(stubNixOS), &hst.Config{Container: &hst.ContainerConfig{
|
||||
{"nixos permissive defaults no enablements", new(stubNixOS), &hst.Config{DirectPipeWire: true, Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Target: fhs.AbsRoot,
|
||||
@@ -252,6 +252,8 @@ func TestOutcomeRun(t *testing.T) {
|
||||
}},
|
||||
|
||||
{"nixos permissive defaults chromium", new(stubNixOS), &hst.Config{
|
||||
DirectPipeWire: true,
|
||||
|
||||
ID: "org.chromium.Chromium",
|
||||
Identity: 9,
|
||||
Groups: []string{"video"},
|
||||
@@ -335,7 +337,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
|
||||
Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711).
|
||||
Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
|
||||
PipeWire(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire")).
|
||||
PipeWire(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
|
||||
MustProxyDBus(&hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.freedesktop.Notifications",
|
||||
@@ -422,6 +424,8 @@ func TestOutcomeRun(t *testing.T) {
|
||||
}},
|
||||
|
||||
{"nixos chromium direct wayland", new(stubNixOS), &hst.Config{
|
||||
DirectPipeWire: true,
|
||||
|
||||
ID: "org.chromium.Chromium",
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Container: &hst.ContainerConfig{
|
||||
@@ -486,7 +490,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
|
||||
UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute).
|
||||
Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711).
|
||||
PipeWire(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire")).
|
||||
PipeWire(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire"), "org.chromium.Chromium", "8e2c76b066dabe574cf073bdb46eb5c1").
|
||||
MustProxyDBus(&hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
|
||||
@@ -879,6 +883,16 @@ func (k *stubNixOS) lookupGroupId(name string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) lookPath(file string) (string, error) {
|
||||
switch file {
|
||||
case "pipewire-pulse":
|
||||
return "/run/current-system/sw/bin/pipewire-pulse", nil
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected file %q", file))
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
|
||||
switch cmd.Path {
|
||||
case "/proc/nonexistent/hsu":
|
||||
|
||||
@@ -14,9 +14,12 @@ import (
|
||||
"time"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/pipewire"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -83,6 +86,55 @@ func Shim(msg message.Msg) {
|
||||
shimEntrypoint(direct{msg})
|
||||
}
|
||||
|
||||
// A shimPrivate holds state of the private work directory owned by shim.
|
||||
type shimPrivate struct {
|
||||
// Path to directory if created.
|
||||
pathname *check.Absolute
|
||||
|
||||
k syscallDispatcher
|
||||
id *stringPair[hst.ID]
|
||||
}
|
||||
|
||||
// unwrap returns the underlying pathname.
|
||||
func (sp *shimPrivate) unwrap() *check.Absolute {
|
||||
if sp.pathname == nil {
|
||||
if a, err := check.NewAbs(sp.k.tempdir()); err != nil {
|
||||
sp.k.fatal(err)
|
||||
panic("unreachable")
|
||||
} else {
|
||||
pathname := a.Append(".hakurei-shim-" + sp.id.String())
|
||||
sp.k.getMsg().Verbosef("creating private work directory %q", pathname)
|
||||
if err = sp.k.mkdir(pathname.String(), 0700); err != nil {
|
||||
sp.k.fatal(err)
|
||||
panic("unreachable")
|
||||
}
|
||||
sp.pathname = pathname
|
||||
return sp.unwrap()
|
||||
}
|
||||
} else {
|
||||
return sp.pathname
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the absolute pathname to the directory held by shimPrivate.
|
||||
func (sp *shimPrivate) String() string { return sp.unwrap().String() }
|
||||
|
||||
// destroy removes the directory held by shimPrivate.
|
||||
func (sp *shimPrivate) destroy() {
|
||||
defer func() { sp.pathname = nil }()
|
||||
if sp.pathname != nil {
|
||||
sp.k.getMsg().Verbosef("destroying private work directory %q", sp.pathname)
|
||||
if err := sp.k.removeAll(sp.pathname.String()); err != nil {
|
||||
sp.k.getMsg().GetLogger().Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// shimPipeWireTimeout is the duration pipewire-pulse is allowed to run before its socket becomes available.
|
||||
shimPipeWireTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
func shimEntrypoint(k syscallDispatcher) {
|
||||
msg := k.getMsg()
|
||||
if msg == nil {
|
||||
@@ -208,6 +260,7 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
|
||||
ctx, stop := k.notifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
cancelContainer.Store(&stop)
|
||||
sp := shimPrivate{k: k, id: state.id}
|
||||
z := container.New(ctx, msg)
|
||||
z.Params = *stateParams.params
|
||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
@@ -215,6 +268,79 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
// bounds and default enforced in finalise.go
|
||||
z.WaitDelay = state.Shim.WaitDelay
|
||||
|
||||
if stateParams.pipewirePulsePath != nil {
|
||||
zpw := container.NewCommand(ctx, msg, stateParams.pipewirePulsePath, pipewirePulseName)
|
||||
zpw.Hostname = "hakurei-" + pipewirePulseName
|
||||
zpw.SeccompFlags |= seccomp.AllowMultiarch
|
||||
zpw.SeccompPresets |= std.PresetStrict
|
||||
zpw.Env = []string{
|
||||
// pipewire SecurityContext socket path
|
||||
pipewire.Remote + "=" + stateParams.instancePath().Append("pipewire").String(),
|
||||
// pipewire-pulse socket directory path
|
||||
envXDGRuntimeDir + "=" + sp.String(),
|
||||
}
|
||||
if msg.IsVerbose() {
|
||||
zpw.Stdin, zpw.Stdout, zpw.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
}
|
||||
zpw.
|
||||
Bind(fhs.AbsRoot, fhs.AbsRoot, 0).
|
||||
Bind(sp.unwrap(), sp.unwrap(), std.BindWritable).
|
||||
Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
|
||||
socketPath := sp.unwrap().Append("pulse", "native")
|
||||
innerSocketPath := stateParams.runtimeDir.Append("pulse", "native")
|
||||
|
||||
if err := k.containerStart(zpw); err != nil {
|
||||
sp.destroy()
|
||||
printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) },
|
||||
"cannot start "+pipewirePulseName+" container:", err)
|
||||
}
|
||||
if err := k.containerServe(zpw); err != nil {
|
||||
sp.destroy()
|
||||
printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) },
|
||||
"cannot configure "+pipewirePulseName+" container:", err)
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
k.new(func(k syscallDispatcher, msg message.Msg) { done <- k.containerWait(zpw) })
|
||||
|
||||
socketTimer := time.NewTimer(shimPipeWireTimeout)
|
||||
for {
|
||||
select {
|
||||
case <-socketTimer.C:
|
||||
sp.destroy()
|
||||
k.fatal(pipewirePulseName + " exceeded deadline before socket appeared")
|
||||
break
|
||||
|
||||
case err := <-done:
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
msg.Verbosef("cannot wait: %v", err)
|
||||
k.exit(127)
|
||||
}
|
||||
sp.destroy()
|
||||
k.fatal(pipewirePulseName + " " + exitError.ProcessState.String())
|
||||
break
|
||||
|
||||
default:
|
||||
if _, err := k.stat(socketPath.String()); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
sp.destroy()
|
||||
k.fatal(err)
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Microsecond)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
z.Bind(socketPath, innerSocketPath, 0)
|
||||
z.Env = append(z.Env, "PULSE_SERVER=unix:"+innerSocketPath.String())
|
||||
}
|
||||
|
||||
if err := k.containerStart(z); err != nil {
|
||||
var f func(v ...any)
|
||||
if logger := msg.GetLogger(); logger != nil {
|
||||
@@ -225,9 +351,11 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
}
|
||||
}
|
||||
printMessageError(f, "cannot start container:", err)
|
||||
sp.destroy()
|
||||
k.exit(hst.ExitFailure)
|
||||
}
|
||||
if err := k.containerServe(z); err != nil {
|
||||
sp.destroy()
|
||||
printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) },
|
||||
"cannot configure container:", err)
|
||||
}
|
||||
@@ -236,10 +364,13 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
seccomp.Preset(std.PresetStrict, seccomp.AllowMultiarch),
|
||||
seccomp.AllowMultiarch,
|
||||
); err != nil {
|
||||
sp.destroy()
|
||||
k.fatalf("cannot load syscall filter: %v", err)
|
||||
}
|
||||
|
||||
if err := k.containerWait(z); err != nil {
|
||||
sp.destroy()
|
||||
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
@@ -250,4 +381,5 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
}
|
||||
k.exit(exitError.ExitCode())
|
||||
}
|
||||
sp.destroy()
|
||||
}
|
||||
|
||||
@@ -3,29 +3,51 @@ package outcome
|
||||
import (
|
||||
"encoding/gob"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/pipewire"
|
||||
)
|
||||
|
||||
func init() { gob.Register(spPipeWireOp{}) }
|
||||
const pipewirePulseName = "pipewire-pulse"
|
||||
|
||||
func init() { gob.Register(new(spPipeWireOp)) }
|
||||
|
||||
// spPipeWireOp exports the PipeWire server to the container via SecurityContext.
|
||||
// Runs after spRuntimeOp.
|
||||
type spPipeWireOp struct{}
|
||||
type spPipeWireOp struct {
|
||||
// Path to pipewire-pulse server. Populated during toSystem if DirectPipeWire is false.
|
||||
CompatServerPath *check.Absolute
|
||||
}
|
||||
|
||||
func (s spPipeWireOp) toSystem(state *outcomeStateSys) error {
|
||||
func (s *spPipeWireOp) toSystem(state *outcomeStateSys) error {
|
||||
if state.et&hst.EPipeWire == 0 {
|
||||
return errNotEnabled
|
||||
}
|
||||
if !state.directPipeWire {
|
||||
if n, err := state.k.lookPath(pipewirePulseName); err != nil {
|
||||
return &hst.AppError{Step: "look up " + pipewirePulseName, Err: err}
|
||||
} else if s.CompatServerPath, err = check.NewAbs(n); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
state.sys.PipeWire(state.instance().Append("pipewire"))
|
||||
appId := state.appId
|
||||
if appId == "" {
|
||||
// use instance ID in case app id is not set
|
||||
appId = "app.hakurei." + state.id.String()
|
||||
}
|
||||
state.sys.PipeWire(state.instance().Append("pipewire"), appId, state.id.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s spPipeWireOp) toContainer(state *outcomeStateParams) error {
|
||||
innerPath := state.runtimeDir.Append(pipewire.PW_DEFAULT_REMOTE)
|
||||
state.env[pipewire.Remote] = innerPath.String()
|
||||
state.params.Bind(state.instancePath().Append("pipewire"), innerPath, 0)
|
||||
func (s *spPipeWireOp) toContainer(state *outcomeStateParams) error {
|
||||
if s.CompatServerPath == nil {
|
||||
innerPath := state.runtimeDir.Append(pipewire.PW_DEFAULT_REMOTE)
|
||||
state.env[pipewire.Remote] = innerPath.String()
|
||||
state.params.Bind(state.instancePath().Append("pipewire"), innerPath, 0)
|
||||
}
|
||||
|
||||
// pipewire-pulse behaviour implemented in shim.go
|
||||
state.pipewirePulsePath = s.CompatServerPath
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestSpPipeWireOp(t *testing.T) {
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"not enabled", func(bool, bool) outcomeOp {
|
||||
return spPipeWireOp{}
|
||||
return new(spPipeWireOp)
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
*c.Enablements = 0
|
||||
@@ -24,13 +24,19 @@ func TestSpPipeWireOp(t *testing.T) {
|
||||
}, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
|
||||
|
||||
{"success", func(bool, bool) outcomeOp {
|
||||
return spPipeWireOp{}
|
||||
}, hst.Template, nil, []stub.Call{}, newI().
|
||||
return new(spPipeWireOp)
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
c.DirectPipeWire = true
|
||||
return c
|
||||
}, nil, []stub.Call{}, newI().
|
||||
// state.instance
|
||||
Ephemeral(system.Process, m(wantInstancePrefix), 0711).
|
||||
// toSystem
|
||||
PipeWire(
|
||||
m(wantInstancePrefix + "/pipewire"),
|
||||
m(wantInstancePrefix+"/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
|
||||
@@ -103,6 +103,8 @@ type Client struct {
|
||||
|
||||
// Populated by [CoreBoundProps] events targeting [Client].
|
||||
Properties SPADict `json:"props"`
|
||||
|
||||
noRemove
|
||||
}
|
||||
|
||||
func (client *Client) consume(opcode byte, files []int, unmarshal func(v any)) error {
|
||||
@@ -113,7 +115,7 @@ func (client *Client) consume(opcode byte, files []int, unmarshal func(v any)) e
|
||||
return nil
|
||||
|
||||
default:
|
||||
return &UnsupportedOpcodeError{opcode, client.String()}
|
||||
panic(&UnsupportedOpcodeError{opcode, client.String()})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,13 +40,14 @@ const (
|
||||
PW_CORE_EVENT_ADD_MEM
|
||||
PW_CORE_EVENT_REMOVE_MEM
|
||||
PW_CORE_EVENT_BOUND_PROPS
|
||||
PW_CORE_EVENT_NUM
|
||||
|
||||
PW_CORE_EVENT_NUM
|
||||
PW_VERSION_CORE_EVENTS = 1
|
||||
)
|
||||
|
||||
const (
|
||||
PW_CORE_METHOD_ADD_LISTENER = iota
|
||||
|
||||
PW_CORE_METHOD_HELLO
|
||||
PW_CORE_METHOD_SYNC
|
||||
PW_CORE_METHOD_PONG
|
||||
@@ -54,25 +55,26 @@ const (
|
||||
PW_CORE_METHOD_GET_REGISTRY
|
||||
PW_CORE_METHOD_CREATE_OBJECT
|
||||
PW_CORE_METHOD_DESTROY
|
||||
PW_CORE_METHOD_NUM
|
||||
|
||||
PW_CORE_METHOD_NUM
|
||||
PW_VERSION_CORE_METHODS = 0
|
||||
)
|
||||
|
||||
const (
|
||||
PW_REGISTRY_EVENT_GLOBAL = iota
|
||||
PW_REGISTRY_EVENT_GLOBAL_REMOVE
|
||||
PW_REGISTRY_EVENT_NUM
|
||||
|
||||
PW_REGISTRY_EVENT_NUM
|
||||
PW_VERSION_REGISTRY_EVENTS = 0
|
||||
)
|
||||
|
||||
const (
|
||||
PW_REGISTRY_METHOD_ADD_LISTENER = iota
|
||||
|
||||
PW_REGISTRY_METHOD_BIND
|
||||
PW_REGISTRY_METHOD_DESTROY
|
||||
PW_REGISTRY_METHOD_NUM
|
||||
|
||||
PW_REGISTRY_METHOD_NUM
|
||||
PW_VERSION_REGISTRY_METHODS = 0
|
||||
)
|
||||
|
||||
@@ -266,6 +268,31 @@ type CoreErrorEvent struct{ CoreError }
|
||||
// Opcode satisfies [Message] with a constant value.
|
||||
func (c *CoreErrorEvent) Opcode() byte { return PW_CORE_EVENT_ERROR }
|
||||
|
||||
// The CoreRemoveId event is used internally by the object ID management logic.
|
||||
//
|
||||
// When a client deletes an object, the server will send this event to acknowledge
|
||||
// that it has seen the delete request. When the client receives this event, it
|
||||
// will know that it can safely reuse the object ID.
|
||||
type CoreRemoveId struct {
|
||||
// A proxy id that was removed.
|
||||
ID Int `json:"id"`
|
||||
}
|
||||
|
||||
// Opcode satisfies [Message] with a constant value.
|
||||
func (c *CoreRemoveId) Opcode() byte { return PW_CORE_EVENT_REMOVE_ID }
|
||||
|
||||
// FileCount satisfies [Message] with a constant value.
|
||||
func (c *CoreRemoveId) FileCount() Int { return 0 }
|
||||
|
||||
// Size satisfies [KnownSize] with a constant value.
|
||||
func (c *CoreRemoveId) Size() Word { return SizePrefix + Size(SizeInt) }
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (c *CoreRemoveId) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *CoreRemoveId) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// The CoreBoundProps event is emitted when a local object ID is bound to a global ID.
|
||||
// It is emitted before the global becomes visible in the registry.
|
||||
type CoreBoundProps struct {
|
||||
@@ -299,13 +326,96 @@ func (c *CoreBoundProps) UnmarshalBinary(data []byte) error { return Unmarshal(d
|
||||
|
||||
// ErrBadBoundProps is returned when a [CoreBoundProps] event targeting a proxy
|
||||
// that should never be targeted is received and processed.
|
||||
var ErrBadBoundProps = errors.New("attempted to store bound props on proxy that should never be targeted")
|
||||
var ErrBadBoundProps = errors.New("attempting to store bound props on a proxy that should never be targeted")
|
||||
|
||||
// noAck is embedded by proxies that are never targeted by [CoreBoundProps].
|
||||
type noAck struct{}
|
||||
|
||||
// setBoundProps should never be called as this proxy should never be targeted by [CoreBoundProps].
|
||||
func (noAck) setBoundProps(*CoreBoundProps) error { return ErrBadBoundProps }
|
||||
func (noAck) setBoundProps(*CoreBoundProps) error { panic(ErrBadBoundProps) }
|
||||
|
||||
// ErrBadRemove is returned when a [CoreRemoveId] event targeting a proxy
|
||||
// that should never be targeted is received and processed.
|
||||
var ErrBadRemove = errors.New("attempting to remove a proxy that should never be targeted")
|
||||
|
||||
// noRemove is embedded by proxies that are never targeted by [CoreRemoveId].
|
||||
type noRemove struct{}
|
||||
|
||||
// remove should never be called as this proxy should never be targeted by [CoreRemoveId].
|
||||
func (noRemove) remove() error { panic(ErrBadRemove) }
|
||||
|
||||
// ErrInvalidRemove is returned when a proxy is somehow removed twice. This is only reached for
|
||||
// an implementation error as the proxy struct should no longer be reachable after the first call.
|
||||
var ErrInvalidRemove = errors.New("attempting to remove an already freed proxy")
|
||||
|
||||
// removable is embedded by proxies that can be targeted by [CoreRemoveId] and requires no cleanup.
|
||||
type removable bool
|
||||
|
||||
// remove checks against removal of a freed proxy and marks the proxy as removed.
|
||||
func (s *removable) remove() error {
|
||||
if *s {
|
||||
panic(ErrInvalidRemove)
|
||||
}
|
||||
*s = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrProxyDestroyed is returned when attempting to use a proxy method when the underlying
|
||||
// proxy has already been targeted by a [CoreRemoveId] event.
|
||||
var ErrProxyDestroyed = errors.New("underlying proxy has been removed")
|
||||
|
||||
// checkDestroy returns [ErrProxyDestroyed] if the current proxy has been destroyed.
|
||||
// Must be called at the beginning of any exported method of a proxy embedding removable.
|
||||
func (s *removable) checkDestroy() error {
|
||||
if *s {
|
||||
// not fatal: the caller is allowed to recover from this and allocate a new proxy
|
||||
return ErrProxyDestroyed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mustCheckDestroy calls checkDestroy and panics if a non-nil error is returned.
|
||||
// This is useful for non-exported methods as they should become unreachable.
|
||||
func (s *removable) mustCheckDestroy() {
|
||||
if err := s.checkDestroy(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// destructible is embedded by proxies that can be targeted by the [CoreRemoveId] event and the
|
||||
// [CoreDestroy] method and requires no cleanup. destructible purposefully does not override
|
||||
// removable.mustCheckDestroy because it is used by unexported methods called during event handling
|
||||
// and are exempt from the destruction check.
|
||||
type destructible struct {
|
||||
destroyed bool
|
||||
|
||||
removable
|
||||
}
|
||||
|
||||
// checkDestroy overrides removable.checkDestroy to also check the destroyed field.
|
||||
func (s *destructible) checkDestroy() error {
|
||||
if s.destroyed {
|
||||
return ErrProxyDestroyed
|
||||
}
|
||||
if err := s.removable.checkDestroy(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// destroy calls removable.checkDestroy then queues a [CoreDestroy] event if it succeeds.
|
||||
func (s *destructible) destroy(ctx *Context, id Int) error {
|
||||
if err := s.checkDestroy(); err != nil {
|
||||
return err
|
||||
}
|
||||
l := len(ctx.pendingDestruction)
|
||||
ctx.pendingDestruction[id] = struct{}{}
|
||||
if len(ctx.pendingDestruction) != l+1 {
|
||||
return ErrProxyDestroyed
|
||||
}
|
||||
s.destroyed = true
|
||||
return ctx.GetCore().destroy(id)
|
||||
}
|
||||
|
||||
// An InconsistentIdError describes an inconsistent state where the server claims an impossible
|
||||
// proxy or global id. This is only generated by the [CoreBoundProps] event.
|
||||
@@ -349,10 +459,10 @@ func (c *CoreHello) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *CoreHello) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// coreHello queues a [CoreHello] message for the PipeWire server.
|
||||
// This method should not be called directly, the New function queues this message.
|
||||
func (ctx *Context) coreHello() error {
|
||||
return ctx.writeMessage(
|
||||
// hello queues a [CoreHello] message for the PipeWire server.
|
||||
// This method should not be called directly, the [New] function queues this message.
|
||||
func (core *Core) hello() error {
|
||||
return core.ctx.writeMessage(
|
||||
PW_ID_CORE,
|
||||
&CoreHello{PW_VERSION_CORE},
|
||||
)
|
||||
@@ -388,12 +498,12 @@ func (c *CoreSync) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *CoreSync) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// coreSync queues a [CoreSync] message for the PipeWire server.
|
||||
// sync queues a [CoreSync] message for the PipeWire server.
|
||||
// This is not safe to use directly, callers should use Sync instead.
|
||||
func (ctx *Context) coreSync(id Int) error {
|
||||
return ctx.writeMessage(
|
||||
func (core *Core) sync(id Int) error {
|
||||
return core.ctx.writeMessage(
|
||||
PW_ID_CORE,
|
||||
&CoreSync{id, CoreSyncSequenceOffset + Int(ctx.sequence)},
|
||||
&CoreSync{id, CoreSyncSequenceOffset + Int(core.ctx.sequence)},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -404,13 +514,13 @@ var ErrNotDone = errors.New("did not receive a Core::Done event targeting previo
|
||||
const (
|
||||
// syncTimeout is the maximum duration [Core.Sync] is allowed to take before
|
||||
// receiving [CoreDone] or failing.
|
||||
syncTimeout = 5 * time.Second
|
||||
syncTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// Sync queues a [CoreSync] message for the PipeWire server and initiates a Roundtrip.
|
||||
func (core *Core) Sync() error {
|
||||
core.done = false
|
||||
if err := core.ctx.coreSync(roundtripSyncID); err != nil {
|
||||
if err := core.sync(roundtripSyncID); err != nil {
|
||||
return err
|
||||
}
|
||||
deadline := time.Now().Add(syncTimeout)
|
||||
@@ -429,6 +539,10 @@ func (core *Core) Sync() error {
|
||||
core.ctx.closeReceivedFiles()
|
||||
return &ProxyFatalError{Err: UnacknowledgedProxyError(slices.Collect(maps.Keys(core.ctx.pendingIds))), ProxyErrs: core.ctx.cloneAsProxyErrors()}
|
||||
}
|
||||
if len(core.ctx.pendingDestruction) != 0 {
|
||||
core.ctx.closeReceivedFiles()
|
||||
return &ProxyFatalError{Err: UnacknowledgedProxyDestructionError(slices.Collect(maps.Keys(core.ctx.pendingDestruction))), ProxyErrs: core.ctx.cloneAsProxyErrors()}
|
||||
}
|
||||
return core.ctx.doSyncComplete()
|
||||
}
|
||||
|
||||
@@ -497,6 +611,89 @@ func (ctx *Context) GetRegistry() (*Registry, error) {
|
||||
)
|
||||
}
|
||||
|
||||
// CoreCreateObject is sent when the client requests to create a
|
||||
// new object from a factory of a certain type.
|
||||
//
|
||||
// The client allocates a new_id for the proxy. The server will
|
||||
// allocate a new resource with the same new_id and from then on,
|
||||
// Methods and Events will be exchanged between the new object of
|
||||
// the given type.
|
||||
type CoreCreateObject struct {
|
||||
// The name of a server factory object to use.
|
||||
FactoryName String `json:"factory_name"`
|
||||
// The type of the object to create, this is also the type of
|
||||
// the interface of the new_id proxy.
|
||||
Type String `json:"type"`
|
||||
// Undocumented, assumed to be the local version of the proxy.
|
||||
Version Int `json:"version"`
|
||||
// Extra properties to create the object.
|
||||
Properties *SPADict `json:"props"`
|
||||
// The proxy id of the new object.
|
||||
NewID Int `json:"new_id"`
|
||||
}
|
||||
|
||||
// Opcode satisfies [Message] with a constant value.
|
||||
func (c *CoreCreateObject) Opcode() byte { return PW_CORE_METHOD_CREATE_OBJECT }
|
||||
|
||||
// FileCount satisfies [Message] with a constant value.
|
||||
func (c *CoreCreateObject) FileCount() Int { return 0 }
|
||||
|
||||
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||
func (c *CoreCreateObject) Size() Word {
|
||||
return SizePrefix +
|
||||
SizeString[Word](c.FactoryName) +
|
||||
SizeString[Word](c.Type) +
|
||||
Size(SizeInt) +
|
||||
c.Properties.Size() +
|
||||
Size(SizeInt)
|
||||
}
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (c *CoreCreateObject) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *CoreCreateObject) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// createObject queues a [CoreCreateObject] message for the PipeWire server.
|
||||
// This is not safe to use directly, callers should use typed wrapper methods on [Registry] instead.
|
||||
func (core *Core) createObject(factoryName, typeName String, version Int, props SPADict, newId Int) error {
|
||||
return core.ctx.writeMessage(
|
||||
PW_ID_CORE,
|
||||
&CoreCreateObject{factoryName, typeName, version, &props, newId},
|
||||
)
|
||||
}
|
||||
|
||||
// CoreDestroy is sent when the client requests to destroy an object.
|
||||
type CoreDestroy struct {
|
||||
// The proxy id of the object to destroy.
|
||||
ID Int `json:"id"`
|
||||
}
|
||||
|
||||
// Opcode satisfies [Message] with a constant value.
|
||||
func (c *CoreDestroy) Opcode() byte { return PW_CORE_METHOD_DESTROY }
|
||||
|
||||
// FileCount satisfies [Message] with a constant value.
|
||||
func (c *CoreDestroy) FileCount() Int { return 0 }
|
||||
|
||||
// Size satisfies [KnownSize] with a constant value.
|
||||
func (c *CoreDestroy) Size() Word { return SizePrefix + Size(SizeInt) }
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (c *CoreDestroy) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *CoreDestroy) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// destroy queues a [CoreDestroy] message for the PipeWire server.
|
||||
// This is not safe to use directly, callers should use the exported method
|
||||
// on the proxy implementation instead.
|
||||
func (core *Core) destroy(id Int) error {
|
||||
return core.ctx.writeMessage(
|
||||
PW_ID_CORE,
|
||||
&CoreDestroy{id},
|
||||
)
|
||||
}
|
||||
|
||||
// A RegistryGlobal event is emitted to notify a client about a new global object.
|
||||
type RegistryGlobal struct {
|
||||
// The global id.
|
||||
@@ -533,6 +730,30 @@ func (c *RegistryGlobal) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *RegistryGlobal) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// A RegistryGlobalRemove event is emitted when a global with id was removed.
|
||||
type RegistryGlobalRemove struct {
|
||||
// The global id that was removed.
|
||||
ID Int `json:"id"`
|
||||
}
|
||||
|
||||
// Opcode satisfies [Message] with a constant value.
|
||||
func (c *RegistryGlobalRemove) Opcode() byte { return PW_REGISTRY_EVENT_GLOBAL_REMOVE }
|
||||
|
||||
// FileCount satisfies [Message] with a constant value.
|
||||
func (c *RegistryGlobalRemove) FileCount() Int { return 0 }
|
||||
|
||||
// Size satisfies [KnownSize] with a constant value.
|
||||
func (c *RegistryGlobalRemove) Size() Word {
|
||||
return SizePrefix +
|
||||
Size(SizeInt)
|
||||
}
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (c *RegistryGlobalRemove) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *RegistryGlobalRemove) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// RegistryBind is sent when the client requests to bind to the
|
||||
// global object with id and use the client proxy with new_id as
|
||||
// the proxy. After this call, methods can be sent to the remote
|
||||
@@ -584,10 +805,70 @@ func (registry *Registry) bind(proxy eventProxy, id, version Int) (Int, error) {
|
||||
)
|
||||
}
|
||||
|
||||
// RegistryDestroy is sent to try to destroy the global object with id.
|
||||
// This might fail when the client does not have permission.
|
||||
type RegistryDestroy struct {
|
||||
// The global id to destroy.
|
||||
ID Int `json:"id"`
|
||||
}
|
||||
|
||||
// Opcode satisfies [Message] with a constant value.
|
||||
func (c *RegistryDestroy) Opcode() byte { return PW_REGISTRY_METHOD_DESTROY }
|
||||
|
||||
// FileCount satisfies [Message] with a constant value.
|
||||
func (c *RegistryDestroy) FileCount() Int { return 0 }
|
||||
|
||||
// Size satisfies [KnownSize] with a constant value.
|
||||
func (c *RegistryDestroy) Size() Word {
|
||||
return SizePrefix +
|
||||
Size(SizeInt)
|
||||
}
|
||||
|
||||
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||
func (c *RegistryDestroy) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||
|
||||
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||
func (c *RegistryDestroy) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||
|
||||
// destroy queues a [RegistryDestroy] message for the PipeWire server.
|
||||
func (registry *Registry) destroy(id Int) error {
|
||||
return registry.ctx.writeMessage(
|
||||
registry.ID,
|
||||
&RegistryDestroy{id},
|
||||
)
|
||||
}
|
||||
|
||||
// Destroy tries to destroy the global object with id.
|
||||
func (registry *Registry) Destroy(id Int) (err error) {
|
||||
asCoreError := registry.ctx.expectsCoreError(registry.ID, &err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = registry.destroy(id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = registry.ctx.GetCore().Sync(); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if coreError := asCoreError(); coreError == nil {
|
||||
return
|
||||
} else {
|
||||
switch syscall.Errno(-coreError.Result) {
|
||||
case syscall.EPERM:
|
||||
return &PermissionError{registry.ID, coreError.Message}
|
||||
|
||||
default:
|
||||
return coreError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An UnsupportedObjectTypeError is the name of a type not known by the server [Registry].
|
||||
type UnsupportedObjectTypeError string
|
||||
|
||||
func (e UnsupportedObjectTypeError) Error() string { return "unsupported object type " + string(e) }
|
||||
func (e UnsupportedObjectTypeError) Error() string { return "unsupported object type " + string(e) }
|
||||
func (e UnsupportedObjectTypeError) Message() string { return e.Error() }
|
||||
|
||||
// Core holds state of [PW_TYPE_INTERFACE_Core].
|
||||
type Core struct {
|
||||
@@ -598,22 +879,24 @@ type Core struct {
|
||||
done bool
|
||||
|
||||
ctx *Context
|
||||
|
||||
noAck
|
||||
noRemove
|
||||
}
|
||||
|
||||
// ErrUnexpectedDone is a [CoreDone] event with unexpected values.
|
||||
var ErrUnexpectedDone = errors.New("multiple Core::Done events targeting Core::Sync")
|
||||
|
||||
// An UnknownBoundIdError describes the server claiming to have bound a proxy id that was never allocated.
|
||||
type UnknownBoundIdError[E any] struct {
|
||||
// An UnknownProxyIdError describes an event targeting a proxy id that was never allocated.
|
||||
type UnknownProxyIdError[E any] struct {
|
||||
// Offending id decoded from Data.
|
||||
Id Int
|
||||
// Event received from the server.
|
||||
Event E
|
||||
}
|
||||
|
||||
func (e *UnknownBoundIdError[E]) Error() string {
|
||||
return "unknown bound proxy id " + strconv.Itoa(int(e.Id))
|
||||
func (e *UnknownProxyIdError[E]) Error() string {
|
||||
return "unknown proxy id " + strconv.Itoa(int(e.Id))
|
||||
}
|
||||
|
||||
// An InvalidPingError is a [CorePing] event targeting a proxy id that was never allocated.
|
||||
@@ -668,6 +951,19 @@ func (core *Core) consume(opcode byte, files []int, unmarshal func(v any)) error
|
||||
unmarshal(&coreError)
|
||||
return &coreError
|
||||
|
||||
case PW_CORE_EVENT_REMOVE_ID:
|
||||
var coreRemoveId CoreRemoveId
|
||||
unmarshal(&coreRemoveId)
|
||||
if proxy, ok := core.ctx.proxy[coreRemoveId.ID]; !ok {
|
||||
// this should never happen so is non-recoverable if it does
|
||||
panic(&UnknownProxyIdError[*CoreRemoveId]{Id: coreRemoveId.ID, Event: &coreRemoveId})
|
||||
} else {
|
||||
delete(core.ctx.proxy, coreRemoveId.ID)
|
||||
// not always populated so this is not checked
|
||||
delete(core.ctx.pendingDestruction, coreRemoveId.ID)
|
||||
return proxy.remove()
|
||||
}
|
||||
|
||||
case PW_CORE_EVENT_BOUND_PROPS:
|
||||
var boundProps CoreBoundProps
|
||||
unmarshal(&boundProps)
|
||||
@@ -675,12 +971,12 @@ func (core *Core) consume(opcode byte, files []int, unmarshal func(v any)) error
|
||||
delete(core.ctx.pendingIds, boundProps.ID)
|
||||
proxy, ok := core.ctx.proxy[boundProps.ID]
|
||||
if !ok {
|
||||
return &UnknownBoundIdError[*CoreBoundProps]{Id: boundProps.ID, Event: &boundProps}
|
||||
return &UnknownProxyIdError[*CoreBoundProps]{Id: boundProps.ID, Event: &boundProps}
|
||||
}
|
||||
return proxy.setBoundProps(&boundProps)
|
||||
|
||||
default:
|
||||
return &UnsupportedOpcodeError{opcode, core.String()}
|
||||
panic(&UnsupportedOpcodeError{opcode, core.String()})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,7 +994,9 @@ type Registry struct {
|
||||
Objects map[Int]RegistryGlobal `json:"objects"`
|
||||
|
||||
ctx *Context
|
||||
|
||||
noAck
|
||||
noRemove
|
||||
}
|
||||
|
||||
// A GlobalIDCollisionError describes a [RegistryGlobal] event stepping on a previous instance of itself.
|
||||
@@ -714,6 +1012,14 @@ func (e *GlobalIDCollisionError) Error() string {
|
||||
" stepping on previous id " + strconv.Itoa(int(e.ID)) + " for " + e.Previous.Type
|
||||
}
|
||||
|
||||
// An UnknownGlobalIDRemoveError describes a [RegistryGlobalRemove] event announcing the removal of
|
||||
// a global id that is not yet known to [Registry] or was already deleted.
|
||||
type UnknownGlobalIDRemoveError Int
|
||||
|
||||
func (e UnknownGlobalIDRemoveError) Error() string {
|
||||
return "Registry::GlobalRemove event targets unknown id " + strconv.Itoa(int(e))
|
||||
}
|
||||
|
||||
func (registry *Registry) consume(opcode byte, files []int, unmarshal func(v any)) error {
|
||||
closeReceivedFiles(files...)
|
||||
switch opcode {
|
||||
@@ -727,8 +1033,21 @@ func (registry *Registry) consume(opcode byte, files []int, unmarshal func(v any
|
||||
registry.Objects[global.ID] = global
|
||||
return nil
|
||||
|
||||
case PW_REGISTRY_EVENT_GLOBAL_REMOVE:
|
||||
var globalRemove RegistryGlobalRemove
|
||||
unmarshal(&globalRemove)
|
||||
// server emits PW_CORE_EVENT_REMOVE_ID events targeting
|
||||
// affected proxies so they do not need to be handled here
|
||||
l := len(registry.Objects)
|
||||
delete(registry.Objects, globalRemove.ID)
|
||||
if len(registry.Objects) != l-1 {
|
||||
// this should never happen so is non-recoverable if it does
|
||||
panic(UnknownGlobalIDRemoveError(globalRemove.ID))
|
||||
}
|
||||
return nil
|
||||
|
||||
default:
|
||||
return &UnsupportedOpcodeError{opcode, registry.String()}
|
||||
panic(&UnsupportedOpcodeError{opcode, registry.String()})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ func TestCoreError(t *testing.T) {
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 0x1b bytes */ 0x1b, 0, 0, 0,
|
||||
/*type: String*/ 8, 0, 0, 0,
|
||||
/* type: String */ 8, 0, 0, 0,
|
||||
|
||||
// value: "no permission to destroy 0\x00"
|
||||
0x6e, 0x6f, 0x20, 0x70,
|
||||
@@ -192,6 +192,24 @@ func TestCoreError(t *testing.T) {
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreRemoveId(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreRemoveId, *pipewire.CoreRemoveId]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 3 */ 3, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CoreRemoveId{
|
||||
ID: 3,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreBoundProps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -288,6 +306,83 @@ func TestCoreGetRegistry(t *testing.T) {
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreCreateObject(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreCreateObject, *pipewire.CoreCreateObject]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x80, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 0x13 bytes */ 0x13, 0, 0, 0,
|
||||
/* type: String */ 8, 0, 0, 0,
|
||||
|
||||
// value: "spa-device-factory\x00"
|
||||
0x73, 0x70, 0x61, 0x2d,
|
||||
0x64, 0x65, 0x76, 0x69,
|
||||
0x63, 0x65, 0x2d, 0x66,
|
||||
0x61, 0x63, 0x74, 0x6f,
|
||||
0x72, 0x79, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
|
||||
/* size: 0x1a bytes */ 0x1a, 0, 0, 0,
|
||||
/* type: String */ 8, 0, 0, 0,
|
||||
|
||||
// value: "PipeWire:Interface:Device\x00"
|
||||
0x50, 0x69, 0x70, 0x65,
|
||||
0x57, 0x69, 0x72, 0x65,
|
||||
0x3a, 0x49, 0x6e, 0x74,
|
||||
0x65, 0x72, 0x66, 0x61,
|
||||
0x63, 0x65, 0x3a, 0x44,
|
||||
0x65, 0x76, 0x69, 0x63,
|
||||
0x65, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 3 */ 3, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size */ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0 */ 0, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0xbad */ 0xad, 0xb, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CoreCreateObject{
|
||||
FactoryName: "spa-device-factory",
|
||||
Type: pipewire.PW_TYPE_INTERFACE_Device,
|
||||
Version: pipewire.PW_VERSION_FACTORY,
|
||||
Properties: &pipewire.SPADict{},
|
||||
NewID: 0xbad,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestCoreDestroy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.CoreDestroy, *pipewire.CoreDestroy]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data */ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 3 */ 3, 0, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.CoreDestroy{
|
||||
ID: 3,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestRegistryGlobal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -745,6 +840,23 @@ func TestRegistryGlobal(t *testing.T) {
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestRegistryGlobalRemove(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.RegistryGlobalRemove, *pipewire.RegistryGlobalRemove]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data*/ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0xbad */ 0xad, 0xb, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.RegistryGlobalRemove{
|
||||
ID: 0xbad,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestRegistryBind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -757,3 +869,20 @@ func TestRegistryBind(t *testing.T) {
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
func TestRegistryDestroy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encodingTestCases[pipewire.RegistryDestroy, *pipewire.RegistryDestroy]{
|
||||
{"sample", []byte{
|
||||
/* size: rest of data*/ 0x10, 0, 0, 0,
|
||||
/* type: Struct */ 0xe, 0, 0, 0,
|
||||
/* size: 4 bytes */ 4, 0, 0, 0,
|
||||
/* type: Int */ 4, 0, 0, 0,
|
||||
/* value: 0xbad */ 0xad, 0xb, 0, 0,
|
||||
/* padding */ 0, 0, 0, 0,
|
||||
}, pipewire.RegistryDestroy{
|
||||
ID: 0xbad,
|
||||
}, nil},
|
||||
}.run(t)
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ package pipewire
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
@@ -26,10 +26,16 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Conn is a low level unix socket interface used by [Context].
|
||||
type Conn interface {
|
||||
// MightBlock informs the implementation that the next call to
|
||||
// Recvmsg or Sendmsg might block. A zero or negative timeout
|
||||
// cancels this behaviour.
|
||||
MightBlock(timeout time.Duration)
|
||||
|
||||
// Recvmsg calls syscall.Recvmsg on the underlying socket.
|
||||
Recvmsg(p, oob []byte, flags int) (n, oobn, recvflags int, err error)
|
||||
|
||||
@@ -51,20 +57,27 @@ type Context struct {
|
||||
buf []byte
|
||||
// Current [Header.Sequence] value, incremented every write.
|
||||
sequence Int
|
||||
// Current server-side [Header.Sequence] value, incremented on every event processed.
|
||||
remoteSequence Int
|
||||
// Pending file descriptors to be sent with the next message.
|
||||
pendingFiles []int
|
||||
// File count already kept track of in [Header].
|
||||
headerFiles int
|
||||
|
||||
// Proxy id associations.
|
||||
proxy map[Int]eventProxy
|
||||
// Newly allocated proxies pending acknowledgement from the server.
|
||||
pendingIds map[Int]struct{}
|
||||
// Smallest available Id for the next proxy.
|
||||
nextId Int
|
||||
// Server side registry generation number.
|
||||
generation Long
|
||||
// Pending file descriptors to be sent with the next message.
|
||||
pendingFiles []int
|
||||
// File count already kept track of in [Header].
|
||||
headerFiles int
|
||||
// Proxies targeted by the [CoreDestroy] event pending until next [CoreSync].
|
||||
pendingDestruction map[Int]struct{}
|
||||
|
||||
// Proxy for built-in core events.
|
||||
core Core
|
||||
// Proxy for built-in client events.
|
||||
client Client
|
||||
|
||||
// Current server-side [Header.Sequence] value, incremented on every event processed.
|
||||
remoteSequence Int
|
||||
// Files from the server. This is discarded on every Roundtrip so eventProxy
|
||||
// implementations must make sure to close them to avoid leaking fds.
|
||||
//
|
||||
@@ -80,13 +93,11 @@ type Context struct {
|
||||
// Pending footer value deferred to the next round trip,
|
||||
// sent if pendingFooter is nil. This is for emulating upstream behaviour
|
||||
deferredPendingFooter KnownSize
|
||||
// Server side registry generation number.
|
||||
generation Long
|
||||
// Deferred operations ran after a [Core.Sync] completes or Close is called. Errors
|
||||
//are reported as part of [ProxyConsumeError] and is not considered fatal unless panicked.
|
||||
syncComplete []func() error
|
||||
// Proxy for built-in core events.
|
||||
core Core
|
||||
// Proxy for built-in client events.
|
||||
client Client
|
||||
|
||||
// Passed to [Conn.Recvmsg]. Not copied if sufficient for all received messages.
|
||||
iovecBuf [1 << 15]byte
|
||||
@@ -120,8 +131,9 @@ func New(conn Conn, props SPADict) (*Context, error) {
|
||||
PW_ID_CLIENT: {},
|
||||
}
|
||||
ctx.nextId = Int(len(ctx.proxy))
|
||||
ctx.pendingDestruction = make(map[Int]struct{})
|
||||
|
||||
if err := ctx.coreHello(); err != nil {
|
||||
if err := ctx.core.hello(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ctx.clientUpdateProperties(props); err != nil {
|
||||
@@ -131,45 +143,142 @@ func New(conn Conn, props SPADict) (*Context, error) {
|
||||
return &ctx, nil
|
||||
}
|
||||
|
||||
// A SyscallConnCloser is a [syscall.Conn] that implements [io.Closer].
|
||||
type SyscallConnCloser interface {
|
||||
syscall.Conn
|
||||
io.Closer
|
||||
// unixConn is an implementation of the [Conn] interface for connections
|
||||
// to Unix domain sockets.
|
||||
type unixConn struct {
|
||||
fd int
|
||||
|
||||
// Whether creation of a new epoll instance was attempted.
|
||||
epoll bool
|
||||
// File descriptor referring to the new epoll instance.
|
||||
// Valid if epoll is true and epollErr is nil.
|
||||
epollFd int
|
||||
// Error returned by syscall.EpollCreate1.
|
||||
epollErr error
|
||||
// Stores epoll events from the kernel.
|
||||
epollBuf [32]syscall.EpollEvent
|
||||
|
||||
// If non-zero, next call is treated as a blocking call.
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// A SyscallConn is a [Conn] adapter for [syscall.Conn].
|
||||
type SyscallConn struct{ SyscallConnCloser }
|
||||
// Dial connects to a Unix domain socket described by name.
|
||||
func Dial(name string) (Conn, error) {
|
||||
if fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC|syscall.SOCK_NONBLOCK, 0); err != nil {
|
||||
return nil, os.NewSyscallError("socket", err)
|
||||
} else if err = syscall.Connect(fd, &syscall.SockaddrUnix{Name: name}); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, os.NewSyscallError("connect", err)
|
||||
} else {
|
||||
return &unixConn{fd: fd}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Recvmsg implements [Conn.Recvmsg] via [syscall.Conn.SyscallConn].
|
||||
func (conn SyscallConn) Recvmsg(p, oob []byte, flags int) (n, oobn, recvflags int, err error) {
|
||||
var rc syscall.RawConn
|
||||
if rc, err = conn.SyscallConn(); err != nil {
|
||||
// MightBlock informs the implementation that the next call
|
||||
// might block for a non-zero timeout.
|
||||
func (conn *unixConn) MightBlock(timeout time.Duration) {
|
||||
if timeout < 0 {
|
||||
timeout = 0
|
||||
}
|
||||
conn.timeout = timeout
|
||||
}
|
||||
|
||||
// wantsEpoll is called at the beginning of any method that might use epoll.
|
||||
func (conn *unixConn) wantsEpoll() error {
|
||||
if !conn.epoll {
|
||||
conn.epoll = true
|
||||
conn.epollFd, conn.epollErr = syscall.EpollCreate1(syscall.EPOLL_CLOEXEC)
|
||||
if conn.epollErr == nil {
|
||||
if conn.epollErr = syscall.EpollCtl(conn.epollFd, syscall.EPOLL_CTL_ADD, conn.fd, &syscall.EpollEvent{
|
||||
Events: syscall.EPOLLERR | syscall.EPOLLHUP,
|
||||
Fd: int32(conn.fd),
|
||||
}); conn.epollErr != nil {
|
||||
_ = syscall.Close(conn.epollFd)
|
||||
}
|
||||
}
|
||||
}
|
||||
return conn.epollErr
|
||||
}
|
||||
|
||||
// wait waits for a specific I/O event on fd. Caller must arrange for wantsEpoll
|
||||
// to be called somewhere before wait is called.
|
||||
func (conn *unixConn) wait(event uint32) (err error) {
|
||||
if conn.timeout == 0 {
|
||||
return nil
|
||||
}
|
||||
deadline := time.Now().Add(conn.timeout)
|
||||
conn.timeout = 0
|
||||
|
||||
if err = syscall.EpollCtl(conn.epollFd, syscall.EPOLL_CTL_MOD, conn.fd, &syscall.EpollEvent{
|
||||
Events: event | syscall.EPOLLERR | syscall.EPOLLHUP,
|
||||
Fd: int32(conn.fd),
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if controlErr := rc.Control(func(fd uintptr) {
|
||||
n, oobn, recvflags, _, err = syscall.Recvmsg(int(fd), p, oob, flags)
|
||||
}); controlErr != nil && err == nil {
|
||||
err = controlErr
|
||||
for timeout := deadline.Sub(time.Now()); timeout > 0; timeout = deadline.Sub(time.Now()) {
|
||||
var n int
|
||||
if n, err = syscall.EpollWait(conn.epollFd, conn.epollBuf[:], int(timeout/time.Millisecond)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch n {
|
||||
case 1: // only the socket fd is ever added
|
||||
if conn.epollBuf[0].Fd != int32(conn.fd) { // unreachable
|
||||
return syscall.ENOTRECOVERABLE
|
||||
}
|
||||
if conn.epollBuf[0].Events&event == event ||
|
||||
conn.epollBuf[0].Events&syscall.EPOLLERR|syscall.EPOLLHUP != 0 {
|
||||
return nil
|
||||
}
|
||||
err = syscall.ETIME
|
||||
continue
|
||||
|
||||
case 0: // timeout
|
||||
return syscall.ETIMEDOUT
|
||||
|
||||
default: // unreachable
|
||||
return syscall.ENOTRECOVERABLE
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Sendmsg implements [Conn.Sendmsg] via [syscall.Conn.SyscallConn].
|
||||
func (conn SyscallConn) Sendmsg(p, oob []byte, flags int) (n int, err error) {
|
||||
var rc syscall.RawConn
|
||||
if rc, err = conn.SyscallConn(); err != nil {
|
||||
// Recvmsg calls syscall.Recvmsg on the underlying socket.
|
||||
func (conn *unixConn) Recvmsg(p, oob []byte, flags int) (n, oobn, recvflags int, err error) {
|
||||
if err = conn.wantsEpoll(); err != nil {
|
||||
return
|
||||
} else if err = conn.wait(syscall.EPOLLIN); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if controlErr := rc.Control(func(fd uintptr) {
|
||||
n, err = syscall.SendmsgN(int(fd), p, oob, nil, flags)
|
||||
}); controlErr != nil && err == nil {
|
||||
err = controlErr
|
||||
}
|
||||
n, oobn, recvflags, _, err = syscall.Recvmsg(conn.fd, p, oob, flags)
|
||||
return
|
||||
}
|
||||
|
||||
// Sendmsg calls syscall.Sendmsg on the underlying socket.
|
||||
func (conn *unixConn) Sendmsg(p, oob []byte, flags int) (n int, err error) {
|
||||
if err = conn.wantsEpoll(); err != nil {
|
||||
return
|
||||
} else if err = conn.wait(syscall.EPOLLOUT); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n, err = syscall.SendmsgN(conn.fd, p, oob, nil, flags)
|
||||
return
|
||||
}
|
||||
|
||||
// Close closes the underlying socket and the epoll fd if populated.
|
||||
func (conn *unixConn) Close() (err error) {
|
||||
if conn.epoll && conn.epollErr == nil {
|
||||
conn.epollErr = syscall.Close(conn.epollFd)
|
||||
}
|
||||
if err = syscall.Close(conn.fd); err != nil {
|
||||
return
|
||||
}
|
||||
return conn.epollErr
|
||||
}
|
||||
|
||||
// MustNew calls [New](conn, props) and panics on error.
|
||||
// It is intended for use in tests with hard-coded strings.
|
||||
func MustNew(conn Conn, props SPADict) *Context {
|
||||
@@ -303,7 +412,7 @@ func (ctx *Context) recvmsg(remaining []byte) (payload []byte, err error) {
|
||||
}
|
||||
if err != syscall.EAGAIN && err != syscall.EWOULDBLOCK {
|
||||
ctx.closeReceivedFiles()
|
||||
return nil, os.NewSyscallError("recvmsg", err)
|
||||
return nil, &ProxyFatalError{Err: os.NewSyscallError("recvmsg", err), ProxyErrs: ctx.cloneAsProxyErrors()}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +449,7 @@ func (ctx *Context) sendmsg(p []byte, fds ...int) error {
|
||||
}
|
||||
|
||||
if err != nil && err != syscall.EAGAIN && err != syscall.EWOULDBLOCK {
|
||||
return os.NewSyscallError("sendmsg", err)
|
||||
return &ProxyFatalError{Err: os.NewSyscallError("sendmsg", err), ProxyErrs: ctx.cloneAsProxyErrors()}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -419,6 +528,9 @@ type eventProxy interface {
|
||||
consume(opcode byte, files []int, unmarshal func(v any)) error
|
||||
// setBoundProps stores a [CoreBoundProps] event received from the server.
|
||||
setBoundProps(event *CoreBoundProps) error
|
||||
// remove is called when the proxy is removed for any reason, usually from
|
||||
// being targeted by a [PW_CORE_EVENT_REMOVE_ID] event.
|
||||
remove() error
|
||||
|
||||
// Stringer returns the PipeWire interface name.
|
||||
fmt.Stringer
|
||||
@@ -499,13 +611,21 @@ func (e DanglingFilesError) Error() string {
|
||||
}
|
||||
|
||||
// An UnacknowledgedProxyError holds newly allocated proxy ids that the server failed
|
||||
// to acknowledge after an otherwise successful [Context.Roundtrip].
|
||||
// to acknowledge after an otherwise successful [Core.Sync].
|
||||
type UnacknowledgedProxyError []Int
|
||||
|
||||
func (e UnacknowledgedProxyError) Error() string {
|
||||
return "server did not acknowledge " + strconv.Itoa(len(e)) + " proxies"
|
||||
}
|
||||
|
||||
// An UnacknowledgedProxyDestructionError holds destroyed proxy ids that the server failed
|
||||
// to acknowledge after an otherwise successful [Core.Sync].
|
||||
type UnacknowledgedProxyDestructionError []Int
|
||||
|
||||
func (e UnacknowledgedProxyDestructionError) Error() string {
|
||||
return "server did not acknowledge " + strconv.Itoa(len(e)) + " proxy destructions"
|
||||
}
|
||||
|
||||
// A ProxyFatalError describes an error that terminates event handling during a
|
||||
// [Context.Roundtrip] and makes further event processing no longer possible.
|
||||
type ProxyFatalError struct {
|
||||
@@ -580,8 +700,15 @@ func (ctx *Context) Roundtrip() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
// roundtripTimeout is the maximum duration socket operations during
|
||||
// Context.roundtrip is allowed to block for.
|
||||
roundtripTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// roundtrip implements the Roundtrip method without checking proxyErrors.
|
||||
func (ctx *Context) roundtrip() (err error) {
|
||||
ctx.conn.MightBlock(roundtripTimeout)
|
||||
if err = ctx.sendmsg(ctx.buf, ctx.pendingFiles...); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -615,6 +742,7 @@ func (ctx *Context) roundtrip() (err error) {
|
||||
}()
|
||||
|
||||
var remaining []byte
|
||||
ctx.conn.MightBlock(roundtripTimeout)
|
||||
for {
|
||||
remaining, err = ctx.consume(remaining)
|
||||
if err == nil {
|
||||
@@ -636,7 +764,7 @@ func (ctx *Context) roundtrip() (err error) {
|
||||
}
|
||||
|
||||
// currentSeq returns the current sequence number.
|
||||
// This must only be called from eventProxy.consume.
|
||||
// This must only be called immediately after queueing a message.
|
||||
func (ctx *Context) currentSeq() Int { return ctx.sequence - 1 }
|
||||
|
||||
// currentRemoteSeq returns the current remote sequence number.
|
||||
@@ -786,6 +914,52 @@ func (ctx *Context) Close() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// expectsCoreError returns a function that inspects an error value and
|
||||
// returns the address of a [CoreError] if it is the only error present
|
||||
// and targets the specified proxy and sequence.
|
||||
//
|
||||
// The behaviour of expectsCoreError is only correct for an empty buf
|
||||
// prior to calling. If buf is not empty, [Core.Sync] is called, with
|
||||
// its return value stored to the value pointed to by errP if not nil,
|
||||
// and the function is not populated.
|
||||
//
|
||||
// The caller must queue a message and call [Core.Sync] immediately
|
||||
// after calling expectsCoreError.
|
||||
func (ctx *Context) expectsCoreError(id Int, errP *error) (asCoreError func() (coreError *CoreError)) {
|
||||
if len(ctx.buf) > 0 {
|
||||
if err := ctx.GetCore().Sync(); err != nil {
|
||||
*errP = err
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
sequence := ctx.sequence
|
||||
return func() (coreError *CoreError) {
|
||||
if proxyErrors, ok := (*errP).(ProxyConsumeError); !ok ||
|
||||
len(proxyErrors) != 1 ||
|
||||
!errors.As(proxyErrors[0], &coreError) ||
|
||||
coreError == nil ||
|
||||
coreError.ID != id ||
|
||||
coreError.Sequence != sequence {
|
||||
// do not return a non-matching CoreError
|
||||
coreError = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// A PermissionError describes an error emitted by the server when trying to
|
||||
// perform an operation that the client has no permission for.
|
||||
type PermissionError struct {
|
||||
// The id of the resource (proxy if emitted by the client) that is in error.
|
||||
ID Int `json:"id"`
|
||||
// An error message.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (*PermissionError) Unwrap() error { return syscall.EPERM }
|
||||
func (e *PermissionError) Error() string { return e.Message }
|
||||
|
||||
// Remote is the environment (sic) with the remote name.
|
||||
const Remote = "PIPEWIRE_REMOTE"
|
||||
|
||||
@@ -793,14 +967,14 @@ const Remote = "PIPEWIRE_REMOTE"
|
||||
|
||||
const DEFAULT_SYSTEM_RUNTIME_DIR = "/run/pipewire"
|
||||
|
||||
// connectName connects to a PipeWire remote by name and returns the [net.UnixConn].
|
||||
func connectName(name string, manager bool) (conn *net.UnixConn, err error) {
|
||||
// connectName connects to a PipeWire remote by name and returns the resulting [Conn].
|
||||
func connectName(name string, manager bool) (conn Conn, err error) {
|
||||
if manager && !strings.HasSuffix(name, "-manager") {
|
||||
return connectName(name+"-manager", false)
|
||||
}
|
||||
|
||||
if path.IsAbs(name) || (len(name) > 0 && name[0] == '@') {
|
||||
return net.DialUnix("unix", nil, &net.UnixAddr{Name: name, Net: "unix"})
|
||||
return Dial(name)
|
||||
} else {
|
||||
runtimeDir, ok := os.LookupEnv("PIPEWIRE_RUNTIME_DIR")
|
||||
if !ok || !path.IsAbs(runtimeDir) {
|
||||
@@ -815,7 +989,7 @@ func connectName(name string, manager bool) (conn *net.UnixConn, err error) {
|
||||
if !ok || !path.IsAbs(runtimeDir) {
|
||||
runtimeDir = DEFAULT_SYSTEM_RUNTIME_DIR
|
||||
}
|
||||
return net.DialUnix("unix", nil, &net.UnixAddr{Name: path.Join(runtimeDir, name), Net: "unix"})
|
||||
return Dial(path.Join(runtimeDir, name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -833,12 +1007,11 @@ func ConnectName(name string, manager bool, props SPADict) (ctx *Context, err er
|
||||
}
|
||||
}
|
||||
|
||||
var conn *net.UnixConn
|
||||
var conn Conn
|
||||
if conn, err = connectName(name, manager); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx, err = New(SyscallConn{conn}, props); err != nil {
|
||||
if ctx, err = New(conn, props); err != nil {
|
||||
ctx = nil
|
||||
_ = conn.Close()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
. "syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/internal/pipewire"
|
||||
@@ -680,9 +681,6 @@ func TestContext(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatalf("SecurityContext.Create: error = %v", err)
|
||||
}
|
||||
if err := ctx.GetCore().Sync(); err != nil {
|
||||
t.Fatalf("Sync: error = %v", err)
|
||||
}
|
||||
|
||||
// none of these should change
|
||||
if coreInfo := ctx.GetCore().Info; !reflect.DeepEqual(coreInfo, &wantCoreInfo0) {
|
||||
@@ -718,6 +716,18 @@ type stubUnixConn struct {
|
||||
current int
|
||||
}
|
||||
|
||||
func (conn *stubUnixConn) MightBlock(timeout time.Duration) {
|
||||
if timeout != 5*time.Second {
|
||||
panic("unexpected timeout " + timeout.String())
|
||||
}
|
||||
if conn.current == 0 ||
|
||||
(conn.samples[conn.current-1].nr == SYS_RECVMSG && conn.samples[conn.current-1].errno == EAGAIN && conn.samples[conn.current].nr == SYS_SENDMSG) ||
|
||||
(conn.samples[conn.current-1].nr == SYS_SENDMSG && conn.samples[conn.current].nr == SYS_RECVMSG) {
|
||||
return
|
||||
}
|
||||
panic("unexpected blocking hint before sample " + strconv.Itoa(conn.current))
|
||||
}
|
||||
|
||||
// nextSample returns the current sample and increments the counter.
|
||||
func (conn *stubUnixConn) nextSample(nr uintptr) (sample *stubUnixConnSample, wantOOB []byte, err error) {
|
||||
sample = &conn.samples[conn.current]
|
||||
@@ -836,6 +846,7 @@ func TestContextErrors(t *testing.T) {
|
||||
|
||||
{"UnexpectedFileCountError", &pipewire.UnexpectedFileCountError{0, -1}, "received -1 files instead of the expected 0"},
|
||||
{"UnacknowledgedProxyError", make(pipewire.UnacknowledgedProxyError, 1<<4), "server did not acknowledge 16 proxies"},
|
||||
{"UnacknowledgedProxyDestructionError", make(pipewire.UnacknowledgedProxyDestructionError, 1<<4), "server did not acknowledge 16 proxy destructions"},
|
||||
{"DanglingFilesError", make(pipewire.DanglingFilesError, 1<<4), "received 16 dangling files"},
|
||||
{"UnexpectedFilesError", pipewire.UnexpectedFilesError(1 << 4), "server message headers claim to have sent more files than actually received"},
|
||||
{"UnexpectedSequenceError", pipewire.UnexpectedSequenceError(1 << 4), "unexpected seq 16"},
|
||||
@@ -862,6 +873,19 @@ func TestContextErrors(t *testing.T) {
|
||||
ID: 0xbad,
|
||||
Sequence: 0xcafe,
|
||||
}, "received Core::Ping seq 51966 targeting unknown proxy id 2989"},
|
||||
|
||||
{"GlobalIDCollisionError", &pipewire.GlobalIDCollisionError{
|
||||
ID: 0xbad,
|
||||
Previous: &pipewire.RegistryGlobal{Type: "PipeWire:Interface:Invalid"},
|
||||
Current: &pipewire.RegistryGlobal{Type: "PipeWire:Interface:NewInvalid"},
|
||||
}, "new Registry::Global event for PipeWire:Interface:NewInvalid stepping on previous id 2989 for PipeWire:Interface:Invalid"},
|
||||
|
||||
{"UnknownGlobalIDRemoveError", pipewire.UnknownGlobalIDRemoveError(0xbad), "Registry::GlobalRemove event targets unknown id 2989"},
|
||||
|
||||
{"PermissionError", &pipewire.PermissionError{
|
||||
ID: 2,
|
||||
Message: "no permission to destroy 0",
|
||||
}, "no permission to destroy 0"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
@@ -384,19 +384,16 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error {
|
||||
return nil
|
||||
|
||||
case reflect.Pointer:
|
||||
if len(data) < SizePrefix {
|
||||
return ErrEOFPrefix
|
||||
}
|
||||
switch SPAKind(binary.NativeEndian.Uint32(data[SizeSPrefix:])) {
|
||||
case SPA_TYPE_None:
|
||||
if ok, err := unmarshalHandleNone(&data, wireSizeP); err != nil {
|
||||
return err
|
||||
} else if ok {
|
||||
v.SetZero()
|
||||
return nil
|
||||
|
||||
default:
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
return unmarshalValue(data, v.Elem(), wireSizeP)
|
||||
}
|
||||
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
return unmarshalValue(data, v.Elem(), wireSizeP)
|
||||
|
||||
case reflect.String:
|
||||
*wireSizeP = 0
|
||||
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_String, wireSizeP); err != nil {
|
||||
@@ -422,6 +419,29 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error {
|
||||
}
|
||||
}
|
||||
|
||||
// unmarshalHandleNone establishes prefix bounds and, for a value of type [SPA_TYPE_None],
|
||||
// validates its size and skips the header. This is for unmarshalling values that can be nil.
|
||||
func unmarshalHandleNone(data *[]byte, wireSizeP *Word) (bool, error) {
|
||||
if len(*data) < SizePrefix {
|
||||
return false, ErrEOFPrefix
|
||||
}
|
||||
|
||||
if SPAKind(binary.NativeEndian.Uint32((*data)[SizeSPrefix:])) != SPA_TYPE_None {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
*wireSizeP = 0
|
||||
if err := unmarshalCheckTypeBounds(data, SPA_TYPE_None, wireSizeP); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if len(*data) != 0 {
|
||||
return true, TrailingGarbageError(*data)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// An InconsistentSizeError describes an inconsistent size prefix encountered
|
||||
// in data passed to [Unmarshal].
|
||||
type InconsistentSizeError struct{ Prefix, Expect Word }
|
||||
|
||||
@@ -26,9 +26,10 @@ const (
|
||||
|
||||
const (
|
||||
PW_SECURITY_CONTEXT_METHOD_ADD_LISTENER = iota
|
||||
PW_SECURITY_CONTEXT_METHOD_CREATE
|
||||
PW_SECURITY_CONTEXT_METHOD_NUM
|
||||
|
||||
PW_SECURITY_CONTEXT_METHOD_CREATE
|
||||
|
||||
PW_SECURITY_CONTEXT_METHOD_NUM
|
||||
PW_VERSION_SECURITY_CONTEXT_METHODS = 0
|
||||
)
|
||||
|
||||
@@ -90,6 +91,8 @@ type SecurityContext struct {
|
||||
GlobalID Int `json:"id"`
|
||||
|
||||
ctx *Context
|
||||
|
||||
destructible
|
||||
}
|
||||
|
||||
// GetSecurityContext queues a [RegistryBind] message for the PipeWire server
|
||||
@@ -108,13 +111,39 @@ func (registry *Registry) GetSecurityContext() (securityContext *SecurityContext
|
||||
}
|
||||
|
||||
// Create queues a [SecurityContextCreate] message for the PipeWire server.
|
||||
func (securityContext *SecurityContext) Create(listenFd, closeFd int, props SPADict) error {
|
||||
func (securityContext *SecurityContext) Create(listenFd, closeFd int, props SPADict) (err error) {
|
||||
if err = securityContext.checkDestroy(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
asCoreError := securityContext.ctx.expectsCoreError(securityContext.ID, &err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// queued in reverse based on upstream behaviour, unsure why
|
||||
offset := securityContext.ctx.queueFiles(closeFd, listenFd)
|
||||
return securityContext.ctx.writeMessage(
|
||||
if err = securityContext.ctx.writeMessage(
|
||||
securityContext.ID,
|
||||
&SecurityContextCreate{ListenFd: offset + 1, CloseFd: offset + 0, Properties: &props},
|
||||
)
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
if err = securityContext.ctx.GetCore().Sync(); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if coreError := asCoreError(); coreError == nil {
|
||||
return
|
||||
} else {
|
||||
switch syscall.Errno(-coreError.Result) {
|
||||
case syscall.EPERM:
|
||||
return &PermissionError{securityContext.ID, coreError.Message}
|
||||
|
||||
default:
|
||||
return coreError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// securityContextCloser holds onto resources associated to the security context.
|
||||
@@ -144,6 +173,9 @@ func (scc *securityContextCloser) Close() (err error) {
|
||||
// BindAndCreate binds a new socket to the specified pathname and pass it to Create.
|
||||
// It returns an [io.Closer] corresponding to [SecurityContextCreate.CloseFd].
|
||||
func (securityContext *SecurityContext) BindAndCreate(pathname string, props SPADict) (io.Closer, error) {
|
||||
if err := securityContext.checkDestroy(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var scc securityContextCloser
|
||||
|
||||
// ensure pathname is available
|
||||
@@ -185,17 +217,19 @@ func (securityContext *SecurityContext) BindAndCreate(pathname string, props SPA
|
||||
}
|
||||
|
||||
func (securityContext *SecurityContext) consume(opcode byte, files []int, _ func(v any)) error {
|
||||
securityContext.mustCheckDestroy()
|
||||
closeReceivedFiles(files...)
|
||||
switch opcode {
|
||||
// SecurityContext does not receive any events
|
||||
|
||||
default:
|
||||
return &UnsupportedOpcodeError{opcode, securityContext.String()}
|
||||
panic(&UnsupportedOpcodeError{opcode, securityContext.String()})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (securityContext *SecurityContext) setBoundProps(event *CoreBoundProps) error {
|
||||
securityContext.mustCheckDestroy()
|
||||
if securityContext.ID != event.ID {
|
||||
return &InconsistentIdError{Proxy: securityContext, ID: securityContext.ID, ServerID: event.ID}
|
||||
}
|
||||
@@ -205,4 +239,9 @@ func (securityContext *SecurityContext) setBoundProps(event *CoreBoundProps) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy destroys this [SecurityContext] proxy.
|
||||
func (securityContext *SecurityContext) Destroy() error {
|
||||
return securityContext.destroy(securityContext.ctx, securityContext.ID)
|
||||
}
|
||||
|
||||
func (securityContext *SecurityContext) String() string { return PW_TYPE_INTERFACE_SecurityContext }
|
||||
|
||||
307
internal/pkg/pkg.go
Normal file
307
internal/pkg/pkg.go
Normal file
@@ -0,0 +1,307 @@
|
||||
// Package pkg provides utilities for packaging software.
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
)
|
||||
|
||||
type (
|
||||
// A Checksum is a SHA-384 checksum computed for a cured [Artifact].
|
||||
Checksum = [sha512.Size384]byte
|
||||
|
||||
// An ID is a unique identifier returned by [Artifact.ID]. This value must
|
||||
// be deterministically determined ahead of time.
|
||||
ID Checksum
|
||||
)
|
||||
|
||||
// MustDecode decodes a string representation of [Checksum] and panics if there
|
||||
// is a decoding error or the resulting data is too short.
|
||||
func MustDecode(s string) (checksum Checksum) {
|
||||
if n, err := base64.URLEncoding.Decode(
|
||||
checksum[:],
|
||||
[]byte(s),
|
||||
); err != nil {
|
||||
panic(err)
|
||||
} else if n != len(Checksum{}) {
|
||||
panic(io.ErrUnexpectedEOF)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// An Artifact is a read-only reference to a piece of data that may be created
|
||||
// deterministically but might not currently be available in memory or on the
|
||||
// filesystem.
|
||||
type Artifact interface {
|
||||
// ID returns a globally unique identifier referring to the current
|
||||
// [Artifact]. This value must be known ahead of time and guaranteed to be
|
||||
// unique without having obtained the full contents of the [Artifact].
|
||||
ID() ID
|
||||
|
||||
// Hash returns the [Checksum] created from the full contents of a cured
|
||||
// [Artifact]. This can be stored for future lookup in a [Cache].
|
||||
//
|
||||
// A call to Hash implicitly cures [Artifact].
|
||||
Hash() (Checksum, error)
|
||||
|
||||
// Pathname returns an absolute pathname to a file or directory populated
|
||||
// with the full contents of [Artifact]. This is the most expensive
|
||||
// operation possible on any [Artifact] and should be avoided if possible.
|
||||
//
|
||||
// A call to Pathname implicitly cures [Artifact].
|
||||
//
|
||||
// Callers must only open files read-only. If [Artifact] is a directory,
|
||||
// files must not be created or removed under this directory.
|
||||
Pathname() (*check.Absolute, error)
|
||||
}
|
||||
|
||||
// A File refers to an [Artifact] backed by a single file.
|
||||
type File interface {
|
||||
// Data returns the full contents of [Artifact].
|
||||
//
|
||||
// Callers must not modify the returned byte slice.
|
||||
Data() ([]byte, error)
|
||||
|
||||
Artifact
|
||||
}
|
||||
|
||||
// FlatEntry is the representation of a directory entry via [Flatten].
|
||||
type FlatEntry struct {
|
||||
Name string // base name of the file
|
||||
Mode fs.FileMode // file mode bits
|
||||
Data []byte // file content or symlink destination
|
||||
}
|
||||
|
||||
// Flatten writes a deterministic representation of the contents of fsys to w.
|
||||
// The resulting data can be hashed to produce a deterministic checksum for the
|
||||
// directory.
|
||||
func Flatten(fsys fs.FS, root string, w io.Writer) error {
|
||||
e := gob.NewEncoder(w)
|
||||
return fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fi fs.FileInfo
|
||||
fi, err = d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ent := FlatEntry{
|
||||
Name: fi.Name(),
|
||||
Mode: fi.Mode(),
|
||||
}
|
||||
if ent.Mode.IsRegular() {
|
||||
if ent.Data, err = fs.ReadFile(fsys, path); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if ent.Mode&fs.ModeSymlink != 0 {
|
||||
var newpath string
|
||||
if newpath, err = fs.ReadLink(fsys, path); err != nil {
|
||||
return err
|
||||
}
|
||||
ent.Data = []byte(newpath)
|
||||
}
|
||||
|
||||
return e.Encode(&ent)
|
||||
})
|
||||
}
|
||||
|
||||
// HashFS returns a checksum produced by hashing the result of [Flatten].
|
||||
func HashFS(fsys fs.FS, root string) (Checksum, error) {
|
||||
h := sha512.New384()
|
||||
if err := Flatten(fsys, root, h); err != nil {
|
||||
return Checksum{}, err
|
||||
}
|
||||
return (Checksum)(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// HashDir returns a checksum produced by hashing the result of [Flatten].
|
||||
func HashDir(pathname *check.Absolute) (Checksum, error) {
|
||||
return HashFS(os.DirFS(pathname.String()), ".")
|
||||
}
|
||||
|
||||
const (
|
||||
// dirIdentifier is the directory name appended to Cache.base for storing
|
||||
// artifacts named after their [ID].
|
||||
dirIdentifier = "identifier"
|
||||
// dirChecksum is the directory name appended to Cache.base for storing
|
||||
// artifacts named after their [Checksum].
|
||||
dirChecksum = "checksum"
|
||||
)
|
||||
|
||||
// Cache is a support layer that implementations of [Artifact] can use to store
|
||||
// cured [Artifact] data in a content addressed fashion.
|
||||
type Cache struct {
|
||||
// Directory where all [Cache] related files are placed.
|
||||
base *check.Absolute
|
||||
|
||||
// Synchronises access to public methods.
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// LoadFile loads the contents of a [File] by its identifier.
|
||||
func (c *Cache) LoadFile(id ID) (
|
||||
pathname *check.Absolute,
|
||||
data []byte,
|
||||
err error,
|
||||
) {
|
||||
pathname = c.base.Append(
|
||||
dirIdentifier,
|
||||
base64.URLEncoding.EncodeToString(id[:]),
|
||||
)
|
||||
|
||||
c.mu.RLock()
|
||||
data, err = os.ReadFile(pathname.String())
|
||||
c.mu.RUnlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// A ChecksumMismatchError describes an [Artifact] with unexpected content.
|
||||
type ChecksumMismatchError struct {
|
||||
// Actual and expected checksums.
|
||||
Got, Want Checksum
|
||||
}
|
||||
|
||||
func (e *ChecksumMismatchError) Error() string {
|
||||
return "got " + base64.URLEncoding.EncodeToString(e.Got[:]) +
|
||||
" instead of " + base64.URLEncoding.EncodeToString(e.Want[:])
|
||||
}
|
||||
|
||||
// pathname returns the content-addressed pathname for a [Checksum].
|
||||
func (c *Cache) pathname(checksum *Checksum) *check.Absolute {
|
||||
return c.base.Append(
|
||||
dirChecksum,
|
||||
base64.URLEncoding.EncodeToString(checksum[:]),
|
||||
)
|
||||
}
|
||||
|
||||
// pathnameIdent returns the identifier-based pathname for an [ID].
|
||||
func (c *Cache) pathnameIdent(id *ID) *check.Absolute {
|
||||
return c.base.Append(
|
||||
dirIdentifier,
|
||||
base64.URLEncoding.EncodeToString(id[:]),
|
||||
)
|
||||
}
|
||||
|
||||
// storeFile stores the contents of a [File]. An optional checksum can be
|
||||
// passed via the result buffer which is used to validate the submitted data.
|
||||
//
|
||||
// If locking is disabled, the caller is responsible for acquiring a write lock
|
||||
// and releasing it after this method returns. This makes LoadOrStoreFile
|
||||
// possible without holding the lock while computing hash for store only.
|
||||
func (c *Cache) storeFile(
|
||||
identifierPathname *check.Absolute,
|
||||
data []byte,
|
||||
buf *Checksum,
|
||||
validate, lock bool,
|
||||
) error {
|
||||
h := sha512.New384()
|
||||
h.Write(data)
|
||||
if validate {
|
||||
if got := (Checksum)(h.Sum(nil)); got != *buf {
|
||||
return &ChecksumMismatchError{got, *buf}
|
||||
}
|
||||
} else {
|
||||
h.Sum(buf[:0])
|
||||
}
|
||||
|
||||
checksumPathname := c.pathname(buf)
|
||||
|
||||
if lock {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
}
|
||||
|
||||
if f, err := os.OpenFile(
|
||||
checksumPathname.String(),
|
||||
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
|
||||
0400,
|
||||
); err != nil {
|
||||
// two artifacts may be backed by the same file
|
||||
if !errors.Is(err, os.ErrExist) {
|
||||
return err
|
||||
}
|
||||
} else if _, err = f.Write(data); err != nil {
|
||||
// do not attempt cleanup: this is content-addressed and a partial
|
||||
// write is caught during integrity check
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Link(
|
||||
checksumPathname.String(),
|
||||
identifierPathname.String(),
|
||||
)
|
||||
}
|
||||
|
||||
// StoreFile stores the contents of a [File]. An optional checksum can be
|
||||
// passed via the result buffer which is used to validate the submitted data.
|
||||
func (c *Cache) StoreFile(
|
||||
id ID,
|
||||
data []byte,
|
||||
buf *Checksum,
|
||||
validate bool,
|
||||
) (pathname *check.Absolute, err error) {
|
||||
pathname = c.pathnameIdent(&id)
|
||||
err = c.storeFile(pathname, data, buf, validate, true)
|
||||
return
|
||||
}
|
||||
|
||||
// LoadOrStoreFile attempts to load the contents of a [File] by its identifier,
|
||||
// and if that file is not present, calls makeData and stores its result
|
||||
// instead. Hash validation behaviour is identical to StoreFile.
|
||||
func (c *Cache) LoadOrStoreFile(
|
||||
id ID,
|
||||
makeData func() ([]byte, error),
|
||||
buf *Checksum,
|
||||
validate bool,
|
||||
) (
|
||||
pathname *check.Absolute,
|
||||
data []byte,
|
||||
store bool,
|
||||
err error,
|
||||
) {
|
||||
pathname = c.pathnameIdent(&id)
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
data, err = os.ReadFile(pathname.String())
|
||||
if err == nil || !errors.Is(err, os.ErrNotExist) {
|
||||
return
|
||||
}
|
||||
store = true
|
||||
|
||||
data, err = makeData()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.storeFile(pathname, data, buf, validate, false)
|
||||
return
|
||||
}
|
||||
|
||||
// New returns the address to a new instance of [Cache].
|
||||
func New(base *check.Absolute) (*Cache, error) {
|
||||
for _, name := range []string{
|
||||
dirIdentifier,
|
||||
dirChecksum,
|
||||
} {
|
||||
if err := os.MkdirAll(base.Append(name).String(), 0700); err != nil &&
|
||||
!errors.Is(err, os.ErrExist) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &Cache{
|
||||
base: base,
|
||||
}, nil
|
||||
}
|
||||
366
internal/pkg/pkg_test.go
Normal file
366
internal/pkg/pkg_test.go
Normal file
@@ -0,0 +1,366 @@
|
||||
package pkg_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/internal/pkg"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const testdata = "" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\xad\x0b\x00" +
|
||||
"\x04" +
|
||||
"\xfe\xfe\x00\x00" +
|
||||
"\xfe\xca\x00\x00"
|
||||
|
||||
testdataChecksum := func() pkg.Checksum {
|
||||
h := sha512.New384()
|
||||
h.Write([]byte(testdata))
|
||||
return (pkg.Checksum)(h.Sum(nil))
|
||||
}()
|
||||
|
||||
testdataChecksumString := base64.URLEncoding.EncodeToString(testdataChecksum[:])
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
early func(t *testing.T, base *check.Absolute)
|
||||
f func(t *testing.T, base *check.Absolute, c *pkg.Cache)
|
||||
check func(t *testing.T, base *check.Absolute)
|
||||
}{
|
||||
{"file", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
wantErrNonexistent := &os.PathError{
|
||||
Op: "open",
|
||||
Path: base.Append(
|
||||
"identifier",
|
||||
testdataChecksumString,
|
||||
).String(),
|
||||
Err: syscall.ENOENT,
|
||||
}
|
||||
if _, _, err := c.LoadFile(testdataChecksum); !reflect.DeepEqual(err, wantErrNonexistent) {
|
||||
t.Fatalf("LoadFile: error = %#v, want %#v", err, wantErrNonexistent)
|
||||
}
|
||||
|
||||
identifier := (pkg.ID)(bytes.Repeat([]byte{
|
||||
0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f,
|
||||
}, 8))
|
||||
wantPathname := base.Append(
|
||||
"identifier",
|
||||
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
|
||||
)
|
||||
identifier0 := (pkg.ID)(bytes.Repeat([]byte{
|
||||
0x71, 0xa7, 0xde, 0x6d, 0xa6, 0xde,
|
||||
}, 8))
|
||||
wantPathname0 := base.Append(
|
||||
"identifier",
|
||||
"cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe",
|
||||
)
|
||||
|
||||
// initial store
|
||||
if pathname, err := c.StoreFile(
|
||||
identifier,
|
||||
[]byte(testdata),
|
||||
&testdataChecksum,
|
||||
true,
|
||||
); err != nil {
|
||||
t.Fatalf("StoreFile: error = %v", err)
|
||||
} else if !pathname.Is(wantPathname) {
|
||||
t.Fatalf("StoreFile: pathname = %q, want %q", pathname, wantPathname)
|
||||
}
|
||||
|
||||
// load or store, identical content
|
||||
if pathname, data, store, err := c.LoadOrStoreFile(identifier0, func() ([]byte, error) {
|
||||
return []byte(testdata), nil
|
||||
}, &testdataChecksum, true); err != nil {
|
||||
t.Fatalf("LoadOrStoreFile: error = %v", err)
|
||||
} else if !pathname.Is(wantPathname0) {
|
||||
t.Fatalf("LoadOrStoreFile: pathname = %q, want %q", pathname, wantPathname0)
|
||||
} else if string(data) != testdata {
|
||||
t.Fatalf("LoadOrStoreFile: data = %x, want %x", data, testdata)
|
||||
} else if !store {
|
||||
t.Fatal("LoadOrStoreFile did not store nonpresent entry")
|
||||
}
|
||||
|
||||
// load or store, existing entry
|
||||
if pathname, data, store, err := c.LoadOrStoreFile(identifier, func() ([]byte, error) {
|
||||
return []byte(testdata), nil
|
||||
}, &testdataChecksum, true); err != nil {
|
||||
t.Fatalf("LoadOrStoreFile: error = %v", err)
|
||||
} else if !pathname.Is(wantPathname) {
|
||||
t.Fatalf("LoadOrStoreFile: pathname = %q, want %q", pathname, wantPathname)
|
||||
} else if string(data) != testdata {
|
||||
t.Fatalf("LoadOrStoreFile: data = %x, want %x", data, testdata)
|
||||
} else if store {
|
||||
t.Fatal("LoadOrStoreFile stored over present entry")
|
||||
}
|
||||
|
||||
// load, existing entry
|
||||
if pathname, data, err := c.LoadFile(identifier0); err != nil {
|
||||
t.Fatalf("LoadFile: error = %v", err)
|
||||
} else if !pathname.Is(wantPathname0) {
|
||||
t.Fatalf("LoadFile: pathname = %q, want %q", pathname, wantPathname0)
|
||||
} else if string(data) != testdata {
|
||||
t.Fatalf("LoadFile: data = %x, want %x", data, testdata)
|
||||
}
|
||||
|
||||
// checksum mismatch
|
||||
wantErrChecksum := &pkg.ChecksumMismatchError{
|
||||
Got: testdataChecksum,
|
||||
}
|
||||
if _, err := c.StoreFile(
|
||||
testdataChecksum,
|
||||
[]byte(testdata),
|
||||
new(pkg.Checksum),
|
||||
true,
|
||||
); !reflect.DeepEqual(err, wantErrChecksum) {
|
||||
t.Fatalf("StoreFile: error = %#v, want %#v", err, wantErrChecksum)
|
||||
}
|
||||
|
||||
// verify failed store
|
||||
if _, _, err := c.LoadFile(testdataChecksum); !reflect.DeepEqual(err, wantErrNonexistent) {
|
||||
t.Fatalf("LoadFile: error = %#v, want %#v", err, wantErrNonexistent)
|
||||
}
|
||||
|
||||
// store, same identifier
|
||||
wantPathnameF := base.Append(
|
||||
"identifier",
|
||||
testdataChecksumString,
|
||||
)
|
||||
if pathname, err := c.StoreFile(
|
||||
testdataChecksum,
|
||||
[]byte(testdata),
|
||||
&testdataChecksum,
|
||||
true,
|
||||
); err != nil {
|
||||
t.Fatalf("StoreFile: error = %v", err)
|
||||
} else if !pathname.Is(wantPathnameF) {
|
||||
t.Fatalf("StoreFile: pathname = %q, want %q", pathname, wantPathnameF)
|
||||
}
|
||||
|
||||
// load, same identifier
|
||||
if pathname, data, err := c.LoadFile(testdataChecksum); err != nil {
|
||||
t.Fatalf("LoadFile: error = %v", err)
|
||||
} else if !pathname.Is(wantPathnameF) {
|
||||
t.Fatalf("LoadFile: pathname = %q, want %q", pathname, wantPathnameF)
|
||||
} else if string(data) != testdata {
|
||||
t.Fatalf("LoadFile: data = %x, want %x", data, testdata)
|
||||
}
|
||||
|
||||
// store without validation
|
||||
wantChecksum := pkg.Checksum{
|
||||
0xbe, 0xc0, 0x21, 0xb4, 0xf3, 0x68,
|
||||
0xe3, 0x06, 0x91, 0x34, 0xe0, 0x12,
|
||||
0xc2, 0xb4, 0x30, 0x70, 0x83, 0xd3,
|
||||
0xa9, 0xbd, 0xd2, 0x06, 0xe2, 0x4e,
|
||||
0x5f, 0x0d, 0x86, 0xe1, 0x3d, 0x66,
|
||||
0x36, 0x65, 0x59, 0x33, 0xec, 0x2b,
|
||||
0x41, 0x34, 0x65, 0x96, 0x68, 0x17,
|
||||
0xa9, 0xc2, 0x08, 0xa1, 0x17, 0x17,
|
||||
}
|
||||
var gotChecksum pkg.Checksum
|
||||
wantPathnameG := base.Append(
|
||||
"identifier",
|
||||
base64.URLEncoding.EncodeToString(wantChecksum[:]),
|
||||
)
|
||||
if pathname, err := c.StoreFile(
|
||||
wantChecksum,
|
||||
[]byte{0},
|
||||
&gotChecksum,
|
||||
false,
|
||||
); err != nil {
|
||||
t.Fatalf("StoreFile: error = %#v", err)
|
||||
} else if !pathname.Is(wantPathnameG) {
|
||||
t.Fatalf("StoreFile: pathname = %q, want %q", pathname, wantPathnameG)
|
||||
} else if gotChecksum != wantChecksum {
|
||||
t.Fatalf("StoreFile: buf = %x, want %x", gotChecksum, wantChecksum)
|
||||
}
|
||||
|
||||
// makeData passthrough
|
||||
var zeroIdent pkg.ID
|
||||
wantErrPassthrough := stub.UniqueError(0xcafe)
|
||||
if _, _, _, err := c.LoadOrStoreFile(zeroIdent, func() ([]byte, error) {
|
||||
return nil, wantErrPassthrough
|
||||
}, new(pkg.Checksum), true); !reflect.DeepEqual(err, wantErrPassthrough) {
|
||||
t.Fatalf("LoadOrStoreFile: error = %#v, want %#v", err, wantErrPassthrough)
|
||||
}
|
||||
|
||||
// verify failed store
|
||||
wantErrNonexistentZero := &os.PathError{
|
||||
Op: "open",
|
||||
Path: base.Append(
|
||||
"identifier",
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
).String(),
|
||||
Err: syscall.ENOENT,
|
||||
}
|
||||
if _, _, err := c.LoadFile(zeroIdent); !reflect.DeepEqual(err, wantErrNonexistentZero) {
|
||||
t.Fatalf("LoadFile: error = %#v, want %#v", err, wantErrNonexistentZero)
|
||||
}
|
||||
}, func(t *testing.T, base *check.Absolute) {
|
||||
wantChecksum := pkg.MustDecode(
|
||||
"lvK4lY9bQUFscHpxqHmiPvptjUwOgn3BFhzCXZMeupkY1n22WUPSuh7pswEvVZrx",
|
||||
)
|
||||
if checksum, err := pkg.HashDir(base); err != nil {
|
||||
t.Fatalf("HashDir: error = %v", err)
|
||||
} else if checksum != wantChecksum {
|
||||
t.Fatalf("HashDir: %v", &pkg.ChecksumMismatchError{
|
||||
Got: checksum,
|
||||
Want: wantChecksum,
|
||||
})
|
||||
}
|
||||
}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := check.MustAbs(t.TempDir())
|
||||
if err := os.Chmod(base.String(), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := filepath.WalkDir(base.String(), func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
if !d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return os.Chmod(path, 0700)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
if c, err := pkg.New(base); err != nil {
|
||||
t.Fatalf("New: error = %v", err)
|
||||
} else {
|
||||
if tc.early != nil {
|
||||
tc.early(t, base)
|
||||
}
|
||||
tc.f(t, base, c)
|
||||
tc.check(t, base)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
fsys fs.FS
|
||||
want pkg.Checksum
|
||||
}{
|
||||
{"sample cache file", fstest.MapFS{
|
||||
".": {Mode: 020000000700},
|
||||
|
||||
"checksum": {Mode: 020000000700},
|
||||
"checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX": {Mode: 0400, Data: []byte{0x0}},
|
||||
"checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq": {Mode: 0400, Data: []byte{0x0, 0x0, 0x0, 0x0, 0xad, 0xb, 0x0, 0x4, 0xfe, 0xfe, 0x0, 0x0, 0xfe, 0xca, 0x0, 0x0}},
|
||||
|
||||
"identifier": {Mode: 020000000700},
|
||||
"identifier/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX": {Mode: 0400, Data: []byte{0x0}},
|
||||
"identifier/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq": {Mode: 0400, Data: []byte{0x0, 0x0, 0x0, 0x0, 0xad, 0xb, 0x0, 0x4, 0xfe, 0xfe, 0x0, 0x0, 0xfe, 0xca, 0x0, 0x0}},
|
||||
"identifier/cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe": {Mode: 0400, Data: []byte{0x0, 0x0, 0x0, 0x0, 0xad, 0xb, 0x0, 0x4, 0xfe, 0xfe, 0x0, 0x0, 0xfe, 0xca, 0x0, 0x0}},
|
||||
"identifier/deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef": {Mode: 0400, Data: []byte{0x0, 0x0, 0x0, 0x0, 0xad, 0xb, 0x0, 0x4, 0xfe, 0xfe, 0x0, 0x0, 0xfe, 0xca, 0x0, 0x0}},
|
||||
}, pkg.MustDecode("lvK4lY9bQUFscHpxqHmiPvptjUwOgn3BFhzCXZMeupkY1n22WUPSuh7pswEvVZrx")},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got, err := pkg.HashFS(tc.fsys, "."); err != nil {
|
||||
t.Fatalf("HashFS: error = %v", err)
|
||||
} else if got != tc.want {
|
||||
t.Fatalf("HashFS: %v", &pkg.ChecksumMismatchError{
|
||||
Got: got,
|
||||
Want: tc.want,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{"ChecksumMismatchError", &pkg.ChecksumMismatchError{
|
||||
Want: (pkg.Checksum)(bytes.Repeat([]byte{
|
||||
0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f,
|
||||
}, 8)),
|
||||
}, "got AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||
" instead of deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := tc.err.Error(); got != tc.want {
|
||||
t.Errorf("Error: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("nonexistent", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wantErr := &os.PathError{
|
||||
Op: "mkdir",
|
||||
Path: container.Nonexistent,
|
||||
Err: syscall.ENOENT,
|
||||
}
|
||||
if _, err := pkg.New(check.MustAbs(container.Nonexistent)); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Errorf("New: error = %#v, want %#v", err, wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("permission", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := check.MustAbs(t.TempDir())
|
||||
if err := os.Chmod(tempDir.String(), 0); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
t.Cleanup(func() {
|
||||
if err = os.Chmod(tempDir.String(), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wantErr := &os.PathError{
|
||||
Op: "mkdir",
|
||||
Path: tempDir.Append("cache").String(),
|
||||
Err: syscall.EACCES,
|
||||
}
|
||||
if _, err := pkg.New(tempDir.Append("cache")); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Errorf("New: error = %#v, want %#v", err, wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
// PipeWire maintains a pipewire socket with SecurityContext attached via [pipewire].
|
||||
// The socket stops accepting connections once the pipe referred to by sync is closed.
|
||||
// The socket is pathname only and is destroyed on revert.
|
||||
func (sys *I) PipeWire(dst *check.Absolute) *I {
|
||||
sys.ops = append(sys.ops, &pipewireOp{nil, dst})
|
||||
func (sys *I) PipeWire(dst *check.Absolute, appID, instanceID string) *I {
|
||||
sys.ops = append(sys.ops, &pipewireOp{nil, dst, appID, instanceID})
|
||||
return sys
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ func (sys *I) PipeWire(dst *check.Absolute) *I {
|
||||
type pipewireOp struct {
|
||||
scc io.Closer
|
||||
dst *check.Absolute
|
||||
|
||||
appID, instanceID string
|
||||
}
|
||||
|
||||
func (p *pipewireOp) Type() hst.Enablement { return Process }
|
||||
@@ -56,12 +58,11 @@ func (p *pipewireOp) apply(sys *I) (err error) {
|
||||
|
||||
if p.scc, err = securityContext.BindAndCreate(p.dst.String(), pipewire.SPADict{
|
||||
{Key: pipewire.PW_KEY_SEC_ENGINE, Value: "app.hakurei"},
|
||||
{Key: pipewire.PW_KEY_SEC_APP_ID, Value: p.appID},
|
||||
{Key: pipewire.PW_KEY_SEC_INSTANCE_ID, Value: p.instanceID},
|
||||
{Key: pipewire.PW_KEY_ACCESS, Value: "restricted"},
|
||||
}); err != nil {
|
||||
return newOpError("pipewire", err, false)
|
||||
} else if err = ctx.GetCore().Sync(); err != nil {
|
||||
_ = p.scc.Close()
|
||||
return newOpError("pipewire", err, false)
|
||||
}
|
||||
|
||||
if err = sys.chmod(p.dst.String(), 0); err != nil {
|
||||
@@ -92,7 +93,9 @@ func (p *pipewireOp) revert(sys *I, _ *Criteria) error {
|
||||
func (p *pipewireOp) Is(o Op) bool {
|
||||
target, ok := o.(*pipewireOp)
|
||||
return ok && p != nil && target != nil &&
|
||||
p.dst.Is(target.dst)
|
||||
p.dst.Is(target.dst) &&
|
||||
p.appID == target.appID &&
|
||||
p.instanceID == target.instanceID
|
||||
}
|
||||
|
||||
func (p *pipewireOp) Path() string { return p.dst.String() }
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/internal/acl"
|
||||
@@ -18,6 +19,8 @@ func TestPipeWireOp(t *testing.T) {
|
||||
checkOpBehaviour(t, checkNoParallel, []opBehaviourTestCase{
|
||||
{"success", 0xbeef, 0xff, &pipewireOp{nil,
|
||||
m(path.Join(t.TempDir(), "pipewire")),
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
}, []stub.Call{
|
||||
call("pipewireConnect", stub.ExpectArgs{}, func() *pipewire.Context {
|
||||
if ctx, err := pipewire.New(&stubPipeWireConn{sendmsg: []string{
|
||||
@@ -154,11 +157,11 @@ func TestPipeWireOp(t *testing.T) {
|
||||
string([]byte{
|
||||
// header: SecurityContext::Create
|
||||
3, 0, 0, 0,
|
||||
0xa8, 0, 0, 1,
|
||||
0x40, 1, 0, 1,
|
||||
5, 0, 0, 0,
|
||||
2, 0, 0, 0,
|
||||
// Struct
|
||||
0xa0, 0, 0, 0,
|
||||
0x38, 1, 0, 0,
|
||||
0xe, 0, 0, 0,
|
||||
// Fd: listen_fd = 1
|
||||
8, 0, 0, 0,
|
||||
@@ -171,12 +174,12 @@ func TestPipeWireOp(t *testing.T) {
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
// Struct: spa_dict
|
||||
0x78, 0, 0, 0,
|
||||
0x10, 1, 0, 0,
|
||||
0xe, 0, 0, 0,
|
||||
// Int: n_items = 2
|
||||
// Int: n_items = 4
|
||||
4, 0, 0, 0,
|
||||
4, 0, 0, 0,
|
||||
4, 0, 0, 0,
|
||||
2, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
// String: key = "pipewire.sec.engine"
|
||||
0x14, 0, 0, 0,
|
||||
@@ -194,6 +197,48 @@ func TestPipeWireOp(t *testing.T) {
|
||||
0x68, 0x61, 0x6b, 0x75,
|
||||
0x72, 0x65, 0x69, 0,
|
||||
0, 0, 0, 0,
|
||||
// String: key = "pipewire.sec.app-id"
|
||||
0x14, 0, 0, 0,
|
||||
8, 0, 0, 0,
|
||||
0x70, 0x69, 0x70, 0x65,
|
||||
0x77, 0x69, 0x72, 0x65,
|
||||
0x2e, 0x73, 0x65, 0x63,
|
||||
0x2e, 0x61, 0x70, 0x70,
|
||||
0x2d, 0x69, 0x64, 0,
|
||||
0, 0, 0, 0,
|
||||
// String: value = "org.chromium.Chromium"
|
||||
0x16, 0, 0, 0,
|
||||
8, 0, 0, 0,
|
||||
0x6f, 0x72, 0x67, 0x2e,
|
||||
0x63, 0x68, 0x72, 0x6f,
|
||||
0x6d, 0x69, 0x75, 0x6d,
|
||||
0x2e, 0x43, 0x68, 0x72,
|
||||
0x6f, 0x6d, 0x69, 0x75,
|
||||
// String: key = "pipewire.sec.instance-id"
|
||||
0x6d, 0, 0, 0,
|
||||
0x19, 0, 0, 0,
|
||||
8, 0, 0, 0,
|
||||
0x70, 0x69, 0x70, 0x65,
|
||||
0x77, 0x69, 0x72, 0x65,
|
||||
0x2e, 0x73, 0x65, 0x63,
|
||||
0x2e, 0x69, 0x6e, 0x73,
|
||||
0x74, 0x61, 0x6e, 0x63,
|
||||
0x65, 0x2d, 0x69, 0x64,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
// String: value = "ebf083d1b175911782d413369b64ce7c"
|
||||
0x21, 0, 0, 0,
|
||||
8, 0, 0, 0,
|
||||
0x65, 0x62, 0x66, 0x30,
|
||||
0x38, 0x33, 0x64, 0x31,
|
||||
0x62, 0x31, 0x37, 0x35,
|
||||
0x39, 0x31, 0x31, 0x37,
|
||||
0x38, 0x32, 0x64, 0x34,
|
||||
0x31, 0x33, 0x33, 0x36,
|
||||
0x39, 0x62, 0x36, 0x34,
|
||||
0x63, 0x65, 0x37, 0x63,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
// String: key = "pipewire.access"
|
||||
0x10, 0, 0, 0,
|
||||
8, 0, 0, 0,
|
||||
@@ -386,29 +431,43 @@ func TestPipeWireOp(t *testing.T) {
|
||||
|
||||
checkOpsBuilder(t, "PipeWire", []opsBuilderTestCase{
|
||||
{"sample", 0xcafe, func(_ *testing.T, sys *I) {
|
||||
sys.PipeWire(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"))
|
||||
sys.PipeWire(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c")
|
||||
}, []Op{&pipewireOp{nil,
|
||||
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
}}, stub.Expect{}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"dst differs", &pipewireOp{nil,
|
||||
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7d/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
}, &pipewireOp{nil,
|
||||
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
}, false},
|
||||
|
||||
{"equals", &pipewireOp{nil,
|
||||
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
}, &pipewireOp{nil,
|
||||
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"sample", &pipewireOp{nil,
|
||||
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
}, Process, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire",
|
||||
`pipewire socket at "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"`},
|
||||
})
|
||||
@@ -439,6 +498,12 @@ type stubPipeWireConn struct {
|
||||
curSendmsg int
|
||||
}
|
||||
|
||||
func (conn *stubPipeWireConn) MightBlock(timeout time.Duration) {
|
||||
if timeout != 5*time.Second {
|
||||
panic("unexpected timeout " + timeout.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Recvmsg marshals and copies a stubMessage prepared ahead of time.
|
||||
func (conn *stubPipeWireConn) Recvmsg(p, _ []byte, _ int) (n, _, recvflags int, err error) {
|
||||
defer func() { conn.curRecvmsg++ }()
|
||||
|
||||
106
nixos.nix
106
nixos.nix
@@ -24,11 +24,38 @@ let
|
||||
getsubuid = userid: appid: userid * 100000 + 10000 + appid;
|
||||
getsubname = userid: appid: "u${toString userid}_a${toString appid}";
|
||||
getsubhome = userid: appid: "${cfg.stateDir}/u${toString userid}/a${toString appid}";
|
||||
|
||||
mountpoints = {
|
||||
${cfg.sharefs.name} = mkIf (cfg.sharefs.source != null) {
|
||||
depends = [ cfg.sharefs.source ];
|
||||
device = "sharefs";
|
||||
fsType = "fuse.sharefs";
|
||||
noCheck = true;
|
||||
options = [
|
||||
"rw"
|
||||
"noexec"
|
||||
"nosuid"
|
||||
"nodev"
|
||||
"noatime"
|
||||
"allow_other"
|
||||
"mkdir"
|
||||
"source=${cfg.sharefs.source}"
|
||||
"setuid=${toString config.users.users.${cfg.sharefs.user}.uid}"
|
||||
"setgid=${toString config.users.groups.${cfg.sharefs.group}.gid}"
|
||||
];
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
imports = [ (import ./options.nix packages) ];
|
||||
|
||||
options = {
|
||||
# Forward declare a dummy option for VM filesystems since the real one won't exist
|
||||
# unless the VM module is actually imported.
|
||||
virtualisation.fileSystems = lib.mkOption { };
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
(
|
||||
@@ -66,9 +93,13 @@ in
|
||||
) "" cfg.users;
|
||||
};
|
||||
|
||||
environment.systemPackages = optional (cfg.sharefs.source != null) cfg.sharefs.package;
|
||||
fileSystems = mountpoints;
|
||||
virtualisation.fileSystems = mountpoints;
|
||||
|
||||
home-manager =
|
||||
let
|
||||
privPackages = mapAttrs (username: userid: {
|
||||
privPackages = mapAttrs (_: userid: {
|
||||
home.packages = foldlAttrs (
|
||||
acc: id: app:
|
||||
[
|
||||
@@ -196,15 +227,6 @@ in
|
||||
}
|
||||
]
|
||||
)
|
||||
++ optional (app.enablements.pipewire && app.pulse) {
|
||||
type = "daemon";
|
||||
dst = if app.mapRealUid then "/run/user/${toString config.users.users.${username}.uid}/pulse/native" else "/run/user/65534/pulse/native";
|
||||
path = cfg.shell;
|
||||
args = [
|
||||
"-lc"
|
||||
"exec pipewire-pulse"
|
||||
];
|
||||
}
|
||||
++ [
|
||||
{
|
||||
type = "bind";
|
||||
@@ -331,25 +353,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
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
100
options.md
100
options.md
@@ -35,7 +35,7 @@ package
|
||||
|
||||
|
||||
*Default:*
|
||||
` <derivation hakurei-static-x86_64-unknown-linux-musl-0.3.1> `
|
||||
` <derivation hakurei-static-x86_64-unknown-linux-musl-0.3.3> `
|
||||
|
||||
|
||||
|
||||
@@ -73,11 +73,11 @@ null or boolean
|
||||
|
||||
|
||||
|
||||
## environment\.hakurei\.apps\.\<name>\.enablements\.pulse
|
||||
## environment\.hakurei\.apps\.\<name>\.enablements\.pipewire
|
||||
|
||||
|
||||
|
||||
Whether to share the PulseAudio socket and cookie\.
|
||||
Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket\.
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ null or boolean
|
||||
|
||||
|
||||
|
||||
Whether to share the Wayland socket\.
|
||||
Whether to share the Wayland server via security-context-v1\.
|
||||
|
||||
|
||||
|
||||
@@ -805,7 +805,97 @@ package
|
||||
|
||||
|
||||
*Default:*
|
||||
` <derivation hakurei-hsu-0.3.1> `
|
||||
` <derivation hakurei-hsu-0.3.3> `
|
||||
|
||||
|
||||
|
||||
## environment\.hakurei\.sharefs\.package
|
||||
|
||||
|
||||
|
||||
The sharefs package to use\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
package
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` <derivation sharefs> `
|
||||
|
||||
|
||||
|
||||
## environment\.hakurei\.sharefs\.group
|
||||
|
||||
|
||||
|
||||
Name of the group to run the sharefs daemon as\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
string
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` "sharefs" `
|
||||
|
||||
|
||||
|
||||
## environment\.hakurei\.sharefs\.name
|
||||
|
||||
|
||||
|
||||
Host path to mount sharefs on\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
string
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` "/sdcard" `
|
||||
|
||||
|
||||
|
||||
## environment\.hakurei\.sharefs\.source
|
||||
|
||||
|
||||
|
||||
Writable backing directory\. Setting this to null disables sharefs\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
null or string
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` null `
|
||||
|
||||
|
||||
|
||||
## environment\.hakurei\.sharefs\.user
|
||||
|
||||
|
||||
|
||||
Name of the user to run the sharefs daemon as\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
string
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` "sharefs" `
|
||||
|
||||
|
||||
|
||||
|
||||
54
options.nix
54
options.nix
@@ -12,13 +12,13 @@ in
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = packages.${pkgs.system}.hakurei;
|
||||
default = packages.${pkgs.stdenv.hostPlatform.system}.hakurei;
|
||||
description = "The hakurei package to use.";
|
||||
};
|
||||
|
||||
hsuPackage = mkOption {
|
||||
type = types.package;
|
||||
default = packages.${pkgs.system}.hsu;
|
||||
default = packages.${pkgs.stdenv.hostPlatform.system}.hsu;
|
||||
description = "The hsu package to use.";
|
||||
};
|
||||
|
||||
@@ -40,6 +40,46 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
sharefs = {
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = packages.${pkgs.stdenv.hostPlatform.system}.sharefs;
|
||||
description = "The sharefs package to use.";
|
||||
};
|
||||
|
||||
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
|
||||
@@ -242,19 +282,11 @@ in
|
||||
type = nullOr bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether to share the PipeWire server via SecurityContext.
|
||||
Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
pulse = mkOption {
|
||||
type = nullOr bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether to run the PulseAudio compatibility daemon.
|
||||
'';
|
||||
};
|
||||
|
||||
share = mkOption {
|
||||
type = nullOr package;
|
||||
default = null;
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
wayland-scanner,
|
||||
xorg,
|
||||
|
||||
# for sharefs
|
||||
fuse3,
|
||||
|
||||
# for hpkg
|
||||
zstd,
|
||||
gnutar,
|
||||
@@ -32,7 +35,7 @@
|
||||
|
||||
buildGoModule rec {
|
||||
pname = "hakurei";
|
||||
version = "0.3.2";
|
||||
version = "0.3.3";
|
||||
|
||||
srcFiltered = builtins.path {
|
||||
name = "${pname}-src";
|
||||
@@ -92,6 +95,7 @@ buildGoModule rec {
|
||||
buildInputs = [
|
||||
libffi
|
||||
libseccomp
|
||||
fuse3
|
||||
acl
|
||||
wayland
|
||||
]
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
virtualisation = {
|
||||
# Hopefully reduces spurious test failures:
|
||||
memorySize = if pkgs.hostPlatform.is32bit then 2046 else 8192;
|
||||
memorySize = if pkgs.stdenv.hostPlatform.is32bit then 2046 else 8192;
|
||||
|
||||
qemu.options = [
|
||||
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
lib,
|
||||
nixosTest,
|
||||
testers,
|
||||
buildFHSEnv,
|
||||
writeShellScriptBin,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
withRace ? false,
|
||||
}:
|
||||
|
||||
nixosTest {
|
||||
testers.nixosTest {
|
||||
name = "hakurei" + (if withRace then "-race" else "");
|
||||
nodes.machine =
|
||||
{ options, pkgs, ... }:
|
||||
|
||||
@@ -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" = {
|
||||
|
||||
@@ -41,7 +41,6 @@ in
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
|
||||
"DISPLAY=unix:/tmp/.X11-unix/X0"
|
||||
"HOME=/var/lib/hakurei/u0/a4"
|
||||
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0"
|
||||
"SHELL=/run/current-system/sw/bin/bash"
|
||||
"TERM=linux"
|
||||
"USER=u0_a4"
|
||||
@@ -49,6 +48,7 @@ in
|
||||
"XDG_RUNTIME_DIR=/run/user/65534"
|
||||
"XDG_SESSION_CLASS=user"
|
||||
"XDG_SESSION_TYPE=wayland"
|
||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
|
||||
];
|
||||
|
||||
fs = fs "dead" {
|
||||
@@ -75,6 +75,7 @@ in
|
||||
"fstab" = fs "80001ff" null null;
|
||||
"hsurc" = fs "80001ff" null null;
|
||||
"fuse.conf" = fs "80001ff" null null;
|
||||
"gai.conf" = fs "80001ff" null null;
|
||||
"group" = fs "180" null "hakurei:x:65534:\n";
|
||||
"host.conf" = fs "80001ff" null null;
|
||||
"hostname" = fs "80001ff" null null;
|
||||
@@ -137,12 +138,8 @@ in
|
||||
user = fs "800001ed" {
|
||||
"65534" = fs "800001c0" {
|
||||
bus = fs "10001fd" null null;
|
||||
pulse = fs "800001c0" {
|
||||
native = fs "10001ff" null null;
|
||||
pid = fs "1a4" null null;
|
||||
} null;
|
||||
pulse = fs "800001c0" { native = fs "10001ff" null null; } null;
|
||||
wayland-0 = fs "1000038" null null;
|
||||
pipewire-0 = fs "1000038" null null;
|
||||
} null;
|
||||
} null;
|
||||
} null;
|
||||
@@ -193,8 +190,6 @@ in
|
||||
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
|
||||
nix = fs "800001ed" {
|
||||
profiles = fs "800001ed" {
|
||||
home-manager = fs "80001ff" null null;
|
||||
home-manager-1-link = fs "80001ff" null null;
|
||||
profile = fs "80001ff" null null;
|
||||
profile-1-link = fs "80001ff" null null;
|
||||
} null;
|
||||
@@ -225,15 +220,14 @@ in
|
||||
(ent "/" ignore ignore ignore ignore ignore)
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10004,gid=10004")
|
||||
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10004,gid=10004")
|
||||
(ent "/tmp/hakurei.0/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10004,gid=10004")
|
||||
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10004,gid=10004")
|
||||
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
|
||||
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
@@ -241,12 +235,13 @@ in
|
||||
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
|
||||
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a4" "/var/lib/hakurei/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a4" "/var/lib/hakurei/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
];
|
||||
|
||||
seccomp = true;
|
||||
|
||||
@@ -49,7 +49,6 @@ in
|
||||
env = [
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus"
|
||||
"HOME=/var/lib/hakurei/u0/a3"
|
||||
"PIPEWIRE_REMOTE=/run/user/1000/pipewire-0"
|
||||
"SHELL=/run/current-system/sw/bin/bash"
|
||||
"TERM=linux"
|
||||
"USER=u0_a3"
|
||||
@@ -57,6 +56,7 @@ in
|
||||
"XDG_RUNTIME_DIR=/run/user/1000"
|
||||
"XDG_SESSION_CLASS=user"
|
||||
"XDG_SESSION_TYPE=wayland"
|
||||
"PULSE_SERVER=unix:/run/user/1000/pulse/native"
|
||||
];
|
||||
|
||||
fs = fs "dead" {
|
||||
@@ -100,6 +100,7 @@ in
|
||||
"fstab" = fs "80001ff" null null;
|
||||
"hsurc" = fs "80001ff" null null;
|
||||
"fuse.conf" = fs "80001ff" null null;
|
||||
"gai.conf" = fs "80001ff" null null;
|
||||
"group" = fs "180" null "hakurei:x:100:\n";
|
||||
"host.conf" = fs "80001ff" null null;
|
||||
"hostname" = fs "80001ff" null null;
|
||||
@@ -162,12 +163,8 @@ in
|
||||
user = fs "800001ed" {
|
||||
"1000" = fs "800001f8" {
|
||||
bus = fs "10001fd" null null;
|
||||
pulse = fs "800001c0" {
|
||||
native = fs "10001ff" null null;
|
||||
pid = fs "1a4" null null;
|
||||
} null;
|
||||
pulse = fs "800001c0" { native = fs "10001ff" null null; } null;
|
||||
wayland-0 = fs "1000038" null null;
|
||||
pipewire-0 = fs "1000038" null null;
|
||||
} null;
|
||||
} null;
|
||||
} null;
|
||||
@@ -216,8 +213,6 @@ in
|
||||
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
|
||||
nix = fs "800001ed" {
|
||||
profiles = fs "800001ed" {
|
||||
home-manager = fs "80001ff" null null;
|
||||
home-manager-1-link = fs "80001ff" null null;
|
||||
profile = fs "80001ff" null null;
|
||||
profile-1-link = fs "80001ff" null null;
|
||||
} null;
|
||||
@@ -252,15 +247,14 @@ in
|
||||
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10003,gid=10003")
|
||||
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10003,gid=10003")
|
||||
(ent "/tmp/hakurei.0/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/tmp/hakurei.0/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10003,gid=10003")
|
||||
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10003,gid=10003")
|
||||
(ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/1000/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
|
||||
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
@@ -268,12 +262,13 @@ in
|
||||
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
|
||||
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a3" "/var/lib/hakurei/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a3" "/var/lib/hakurei/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
];
|
||||
|
||||
seccomp = true;
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"fstab" = fs "80001ff" null null;
|
||||
"hsurc" = fs "80001ff" null null;
|
||||
"fuse.conf" = fs "80001ff" null null;
|
||||
"gai.conf" = fs "80001ff" null null;
|
||||
"group" = fs "180" null "hakurei:x:65534:\n";
|
||||
"host.conf" = fs "80001ff" null null;
|
||||
"hostname" = fs "80001ff" null null;
|
||||
@@ -139,23 +140,23 @@
|
||||
|
||||
mount = [
|
||||
(ent "/sysroot" "/" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10000,gid=10000")
|
||||
(ent "/bin" "/bin" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/home" "/home" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/lib64" "/lib64" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/lost+found" "/lost+found" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/nix" "/nix" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/bin" "/bin" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/home" "/home" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/lib64" "/lib64" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/lost+found" "/lost+found" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/nix" "/nix" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/nix/.ro-store" "rw,nosuid,nodev,relatime" "9p" "nix-store" ignore)
|
||||
(ent "/" "/nix/.rw-store" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,mode=755")
|
||||
(ent "/" "/nix/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
|
||||
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
|
||||
(ent "/root" "/root" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/root" "/root" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/run" "rw,nosuid,nodev" "tmpfs" "tmpfs" ignore)
|
||||
(ent "/" "/run/keys" "rw,nosuid,nodev,relatime" "ramfs" "ramfs" "rw,mode=750")
|
||||
(ent "/" "/run/credentials/systemd-journald.service" "ro,nosuid,nodev,noexec,relatime,nosymfollow" "tmpfs" "tmpfs" "rw,size=1024k,nr_inodes=1024,mode=700,noswap")
|
||||
(ent "/" "/run/wrappers" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
|
||||
(ent "/" "/run/credentials/getty@tty1.service" "ro,nosuid,nodev,noexec,relatime,nosymfollow" "tmpfs" "tmpfs" "rw,size=1024k,nr_inodes=1024,mode=700,noswap")
|
||||
(ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
|
||||
(ent "/srv" "/srv" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/srv" "/srv" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/sys" "rw,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/" "/sys/kernel/security" "rw,nosuid,nodev,noexec,relatime" "securityfs" "securityfs" "rw")
|
||||
(ent "/../../.." "/sys/fs/cgroup" "rw,nosuid,nodev,noexec,relatime" "cgroup2" "cgroup2" "rw,nsdelegate,memory_recursiveprot")
|
||||
@@ -166,8 +167,8 @@
|
||||
(ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw")
|
||||
(ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw")
|
||||
(ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw")
|
||||
(ent "/usr" "/usr" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var" "/var" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/usr" "/usr" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/var" "/var" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
|
||||
(ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10000,gid=10000")
|
||||
(ent "/" "/dev" "ro,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=10000,gid=10000")
|
||||
@@ -182,12 +183,12 @@
|
||||
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10000,gid=10000")
|
||||
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10000,gid=10000")
|
||||
(ent "/tmp/hakurei.0/runtime/0" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/tmpdir/0" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/runtime/0" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/tmp/hakurei.0/tmpdir/0" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10000,gid=10000")
|
||||
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10000,gid=10000")
|
||||
(ent "/kvm" "/dev/kvm" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=10000,gid=10000")
|
||||
(ent "/" "/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=10000,gid=10000")
|
||||
(ent "/" "/run/dbus" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=10000,gid=10000")
|
||||
|
||||
@@ -49,7 +49,6 @@ in
|
||||
env = [
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
|
||||
"HOME=/var/lib/hakurei/u0/a5"
|
||||
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0"
|
||||
"SHELL=/run/current-system/sw/bin/bash"
|
||||
"TERM=linux"
|
||||
"USER=u0_a5"
|
||||
@@ -57,6 +56,7 @@ in
|
||||
"XDG_RUNTIME_DIR=/run/user/65534"
|
||||
"XDG_SESSION_CLASS=user"
|
||||
"XDG_SESSION_TYPE=wayland"
|
||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
|
||||
];
|
||||
|
||||
fs = fs "dead" {
|
||||
@@ -98,6 +98,7 @@ in
|
||||
"fstab" = fs "80001ff" null null;
|
||||
"hsurc" = fs "80001ff" null null;
|
||||
"fuse.conf" = fs "80001ff" null null;
|
||||
"gai.conf" = fs "80001ff" null null;
|
||||
"group" = fs "180" null "hakurei:x:65534:\n";
|
||||
"host.conf" = fs "80001ff" null null;
|
||||
"hostname" = fs "80001ff" null null;
|
||||
@@ -160,12 +161,8 @@ in
|
||||
user = fs "800001ed" {
|
||||
"65534" = fs "800001f8" {
|
||||
bus = fs "10001fd" null null;
|
||||
pulse = fs "800001c0" {
|
||||
native = fs "10001ff" null null;
|
||||
pid = fs "1a4" null null;
|
||||
} null;
|
||||
pulse = fs "800001c0" { native = fs "10001ff" null null; } null;
|
||||
wayland-0 = fs "1000038" null null;
|
||||
pipewire-0 = fs "1000038" null null;
|
||||
} null;
|
||||
} null;
|
||||
} null;
|
||||
@@ -214,8 +211,6 @@ in
|
||||
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
|
||||
nix = fs "800001ed" {
|
||||
profiles = fs "800001ed" {
|
||||
home-manager = fs "80001ff" null null;
|
||||
home-manager-1-link = fs "80001ff" null null;
|
||||
profile = fs "80001ff" null null;
|
||||
profile-1-link = fs "80001ff" null null;
|
||||
} null;
|
||||
@@ -250,15 +245,14 @@ in
|
||||
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10005,gid=10005")
|
||||
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10005,gid=10005")
|
||||
(ent "/tmp/hakurei.0/runtime/5" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/tmpdir/5" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/runtime/5" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/tmp/hakurei.0/tmpdir/5" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10005,gid=10005")
|
||||
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10005,gid=10005")
|
||||
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
|
||||
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
@@ -266,9 +260,10 @@ in
|
||||
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a5" "/var/lib/hakurei/u0/a5" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a5" "/var/lib/hakurei/u0/a5" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
];
|
||||
|
||||
seccomp = true;
|
||||
|
||||
@@ -49,7 +49,6 @@ in
|
||||
env = [
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
|
||||
"HOME=/var/lib/hakurei/u0/a1"
|
||||
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0"
|
||||
"SHELL=/run/current-system/sw/bin/bash"
|
||||
"TERM=linux"
|
||||
"USER=u0_a1"
|
||||
@@ -57,6 +56,7 @@ in
|
||||
"XDG_RUNTIME_DIR=/run/user/65534"
|
||||
"XDG_SESSION_CLASS=user"
|
||||
"XDG_SESSION_TYPE=wayland"
|
||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
|
||||
];
|
||||
|
||||
fs = fs "dead" {
|
||||
@@ -97,6 +97,7 @@ in
|
||||
"fstab" = fs "80001ff" null null;
|
||||
"hsurc" = fs "80001ff" null null;
|
||||
"fuse.conf" = fs "80001ff" null null;
|
||||
"gai.conf" = fs "80001ff" null null;
|
||||
"group" = fs "180" null "hakurei:x:65534:\n";
|
||||
"host.conf" = fs "80001ff" null null;
|
||||
"hostname" = fs "80001ff" null null;
|
||||
@@ -159,12 +160,8 @@ in
|
||||
user = fs "800001ed" {
|
||||
"65534" = fs "800001c0" {
|
||||
bus = fs "10001fd" null null;
|
||||
pulse = fs "800001c0" {
|
||||
native = fs "10001ff" null null;
|
||||
pid = fs "1a4" null null;
|
||||
} null;
|
||||
pulse = fs "800001c0" { native = fs "10001ff" null null; } null;
|
||||
wayland-0 = fs "1000038" null null;
|
||||
pipewire-0 = fs "1000038" null null;
|
||||
} null;
|
||||
} null;
|
||||
} null;
|
||||
@@ -213,8 +210,6 @@ in
|
||||
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
|
||||
nix = fs "800001ed" {
|
||||
profiles = fs "800001ed" {
|
||||
home-manager = fs "80001ff" null null;
|
||||
home-manager-1-link = fs "80001ff" null null;
|
||||
profile = fs "80001ff" null null;
|
||||
profile-1-link = fs "80001ff" null null;
|
||||
} null;
|
||||
@@ -251,11 +246,10 @@ in
|
||||
(ent "/" "/tmp" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10001,gid=10001")
|
||||
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10001,gid=10001")
|
||||
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10001,gid=10001")
|
||||
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
|
||||
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
@@ -263,9 +257,10 @@ in
|
||||
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a1" "/var/lib/hakurei/u0/a1" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a1" "/var/lib/hakurei/u0/a1" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
];
|
||||
|
||||
seccomp = true;
|
||||
|
||||
@@ -50,7 +50,6 @@ in
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
|
||||
"DISPLAY=:0"
|
||||
"HOME=/var/lib/hakurei/u0/a2"
|
||||
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0"
|
||||
"SHELL=/run/current-system/sw/bin/bash"
|
||||
"TERM=linux"
|
||||
"USER=u0_a2"
|
||||
@@ -58,6 +57,7 @@ in
|
||||
"XDG_RUNTIME_DIR=/run/user/65534"
|
||||
"XDG_SESSION_CLASS=user"
|
||||
"XDG_SESSION_TYPE=wayland"
|
||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
|
||||
];
|
||||
|
||||
fs = fs "dead" {
|
||||
@@ -102,6 +102,7 @@ in
|
||||
"fstab" = fs "80001ff" null null;
|
||||
"hsurc" = fs "80001ff" null null;
|
||||
"fuse.conf" = fs "80001ff" null null;
|
||||
"gai.conf" = fs "80001ff" null null;
|
||||
"group" = fs "180" null "hakurei:x:65534:\n";
|
||||
"host.conf" = fs "80001ff" null null;
|
||||
"hostname" = fs "80001ff" null null;
|
||||
@@ -164,12 +165,8 @@ in
|
||||
user = fs "800001ed" {
|
||||
"65534" = fs "800001f8" {
|
||||
bus = fs "10001fd" null null;
|
||||
pulse = fs "800001c0" {
|
||||
native = fs "10001ff" null null;
|
||||
pid = fs "1a4" null null;
|
||||
} null;
|
||||
pulse = fs "800001c0" { native = fs "10001ff" null null; } null;
|
||||
wayland-0 = fs "1000038" null null;
|
||||
pipewire-0 = fs "1000038" null null;
|
||||
} null;
|
||||
} null;
|
||||
} null;
|
||||
@@ -220,8 +217,6 @@ in
|
||||
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
|
||||
nix = fs "800001ed" {
|
||||
profiles = fs "800001ed" {
|
||||
home-manager = fs "80001ff" null null;
|
||||
home-manager-1-link = fs "80001ff" null null;
|
||||
profile = fs "80001ff" null null;
|
||||
profile-1-link = fs "80001ff" null null;
|
||||
} null;
|
||||
@@ -257,16 +252,15 @@ in
|
||||
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10002,gid=10002")
|
||||
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10002,gid=10002")
|
||||
(ent "/tmp/hakurei.0/runtime/2" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/runtime/2" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/tmp" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10002,gid=10002")
|
||||
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10002,gid=10002")
|
||||
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10002,gid=10002")
|
||||
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
|
||||
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
@@ -274,12 +268,13 @@ in
|
||||
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
|
||||
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,uuid=on,userxattr")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a2" "/var/lib/hakurei/u0/a2" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a2" "/var/lib/hakurei/u0/a2" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
|
||||
];
|
||||
|
||||
seccomp = true;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}:
|
||||
let
|
||||
testProgram = pkgs.callPackage ./tool/package.nix { inherit (config.environment.hakurei.package) version; };
|
||||
testCases = import ./case pkgs.system lib testProgram;
|
||||
testCases = import ./case pkgs.stdenv.hostPlatform.system lib testProgram;
|
||||
in
|
||||
{
|
||||
users.users = {
|
||||
@@ -33,7 +33,7 @@ in
|
||||
hakurei -v run hakurei-test \
|
||||
-p "/var/tmp/.hakurei-check-ok.0" \
|
||||
-t ${toString (builtins.toFile "hakurei-pd-want.json" (builtins.toJSON testCases.pd.want))} \
|
||||
-s ${testCases.pd.expectedFilter.${pkgs.system}} "$@"
|
||||
-s ${testCases.pd.expectedFilter.${pkgs.stdenv.hostPlatform.system}} "$@"
|
||||
'')
|
||||
];
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
lib,
|
||||
nixosTest,
|
||||
testers,
|
||||
|
||||
self,
|
||||
withRace ? false,
|
||||
}:
|
||||
|
||||
nixosTest {
|
||||
testers.nixosTest {
|
||||
name = "hakurei-sandbox" + (if withRace then "-race" else "");
|
||||
nodes.machine =
|
||||
{ options, pkgs, ... }:
|
||||
|
||||
@@ -79,8 +79,10 @@ check_sandbox("device")
|
||||
check_sandbox("pdlike")
|
||||
|
||||
# Exit Sway and verify process exit status 0:
|
||||
machine.wait_until_fails("pgrep -x hakurei", timeout=5)
|
||||
swaymsg("exit", succeed=False)
|
||||
machine.wait_for_file("/tmp/sway-exit-ok")
|
||||
|
||||
# Print hakurei runDir contents:
|
||||
print(machine.fail("ls /run/user/1000/hakurei"))
|
||||
machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")
|
||||
|
||||
12
test/test.py
12
test/test.py
@@ -226,15 +226,17 @@ machine.send_chars("clear; pactl info && touch /var/tmp/pulse-ok\n")
|
||||
machine.wait_for_file("/var/tmp/pulse-ok", timeout=15)
|
||||
collect_state_ui("pulse_wayland")
|
||||
check_state("pa-foot", {"wayland": True, "pipewire": True})
|
||||
# Test PipeWire:
|
||||
machine.send_chars("clear; pw-cli i 0 && touch /var/tmp/pw-ok\n")
|
||||
machine.wait_for_file("/var/tmp/pw-ok", timeout=15)
|
||||
collect_state_ui("pipewire_wayland")
|
||||
machine.fail("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")
|
||||
machine.send_chars("exit\n")
|
||||
machine.wait_until_fails("pgrep foot", timeout=5)
|
||||
machine.wait_until_fails("pgrep -x hakurei", timeout=5)
|
||||
machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")
|
||||
# Test PipeWire SecurityContext:
|
||||
machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pulse pactl info")
|
||||
machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pulse pactl set-sink-mute @DEFAULT_SINK@ toggle")
|
||||
# Test PipeWire direct access:
|
||||
machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 pw-dump")
|
||||
machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pipewire pw-dump")
|
||||
|
||||
# Test XWayland (foot does not support X):
|
||||
swaymsg("exec x11-alacritty")
|
||||
@@ -280,6 +282,7 @@ machine.send_key("ctrl-c")
|
||||
machine.wait_until_fails("pgrep foot", timeout=5)
|
||||
|
||||
# Exit Sway and verify process exit status 0:
|
||||
machine.wait_until_fails("pgrep -x hakurei", timeout=5)
|
||||
swaymsg("exit", succeed=False)
|
||||
machine.wait_for_file("/tmp/sway-exit-ok")
|
||||
|
||||
@@ -289,6 +292,7 @@ print(machine.succeed("find /tmp/hakurei.0 "
|
||||
+ "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
|
||||
+ "-print"))
|
||||
print(machine.succeed("find /run/user/1000/hakurei"))
|
||||
machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")
|
||||
|
||||
# Verify go test status:
|
||||
machine.wait_for_file("/tmp/hakurei-test-done")
|
||||
|
||||
Reference in New Issue
Block a user