aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--cmd/dist/main.go37
-rw-r--r--cmd/hakurei/command.go8
-rw-r--r--cmd/hakurei/command_test.go6
-rw-r--r--cmd/hakurei/testsuite/sandbox/main.go191
-rw-r--r--hst/container.go7
-rw-r--r--internal/outcome/outcome.go9
-rw-r--r--internal/outcome/shim.go7
-rw-r--r--internal/testsuite/testsuite.go361
-rw-r--r--internal/wayland/security-context-v1-protocol.c2
-rw-r--r--internal/wayland/security-context-v1-protocol.h2
10 files changed, 433 insertions, 197 deletions
diff --git a/cmd/dist/main.go b/cmd/dist/main.go
index 1034f282..40a0583c 100644
--- a/cmd/dist/main.go
+++ b/cmd/dist/main.go
@@ -4,6 +4,7 @@ package main
import (
"archive/tar"
+ "bytes"
"compress/gzip"
"context"
"crypto/sha512"
@@ -57,6 +58,7 @@ func main() {
verbose := os.Getenv("VERBOSE") != ""
runTests := os.Getenv("HAKUREI_DIST_MAKE") == ""
race := os.Getenv("HAKUREI_RACE") != ""
+ dynamic := os.Getenv("HAKUREI_DYNAMIC") != ""
version = getenv("HAKUREI_VERSION", strings.TrimSpace(version))
prefix := getenv("PREFIX", "/usr/local")
destdir := getenv("DESTDIR", "dist")
@@ -69,6 +71,12 @@ func main() {
version += "-race"
}
+ if err := os.Setenv("CC", "clang"); err != nil {
+ log.Fatal(err)
+ } else if err = os.Setenv("CXX", "clang++"); err != nil {
+ log.Fatal(err)
+ }
+
if err := os.MkdirAll(destdir, 0755); err != nil {
log.Fatal(err)
}
@@ -94,15 +102,36 @@ func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
+ const stubFlag = "-buildvcs=false"
verboseFlag := "-v"
if !verbose {
- verboseFlag = "-buildvcs=false"
+ verboseFlag = stubFlag
}
raceFlag := "-race"
if !race {
- raceFlag = "-buildvcs=false"
+ raceFlag = stubFlag
+ }
+
+ ldflagsStatic := " -extldflags=-static"
+ if dynamic {
+ cmd := exec.CommandContext(ctx, "llvm-config", "--host-target")
+ cmd.Stderr = os.Stderr
+ var p []byte
+ p, err = cmd.Output()
+ if err != nil {
+ log.Fatal(err)
+ }
+ p = bytes.TrimSpace(p)
+ _, abi, ok := strings.CutLast(string(p), "-")
+ if !ok {
+ log.Fatalf("invalid target %q", string(p))
+ }
+ version += "-" + abi
+
+ log.Println("WARNING: dynamic linking is not supported")
+ ldflagsStatic = ""
}
log.Printf("Building hakurei %s for %s/%s.", version, runtime.GOOS, runtime.GOARCH)
@@ -112,7 +141,7 @@ func main() {
"-trimpath",
verboseFlag, raceFlag, "-o", s,
"-ldflags=-s -w "+
- "-buildid= -linkmode external -extldflags=-static "+
+ "-buildid= -linkmode external"+ldflagsStatic+" "+
"-X hakurei.app/internal/info.buildVersion="+version+" "+
"-X hakurei.app/internal/info.hakureiPath="+prefix+"/bin/hakurei "+
"-X hakurei.app/internal/info.hsuPath="+prefix+"/bin/hsu",
@@ -136,7 +165,7 @@ func main() {
log.Println("##### Testing Hakurei.")
mustRun(
ctx, nil, "go", "test", raceFlag,
- "-ldflags=-buildid= -linkmode external -extldflags=-static",
+ "-ldflags=-buildid= -linkmode external"+ldflagsStatic,
"./...",
)
log.Println()
diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go
index 76004d4f..f14945ad 100644
--- a/cmd/hakurei/command.go
+++ b/cmd/hakurei/command.go
@@ -106,7 +106,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagSchedPolicy string
flagSchedPriority int
- flagPrivateRuntime, flagPrivateTmpdir bool
+ flagNoWait, flagPrivateRuntime, flagPrivateTmpdir bool
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
)
@@ -194,6 +194,10 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
},
}
+ if flagNoWait {
+ config.Container.AdoptWaitDelay = -1
+ }
+
if err := config.SchedPolicy.UnmarshalText(
[]byte(flagSchedPolicy),
); err != nil {
@@ -315,6 +319,8 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
"Scheduling policy to set for the container").
Flag(&flagSchedPriority, "priority", command.IntFlag(0),
"Scheduling priority to set for the container").
+ Flag(&flagNoWait, "no-wait", command.BoolFlag(false),
+ "Do not wait for lingering processes to terminate").
Flag(&flagPrivateRuntime, "private-runtime", command.BoolFlag(false),
"Do not share XDG_RUNTIME_DIR between containers under the same identity").
Flag(&flagPrivateTmpdir, "private-tmpdir", command.BoolFlag(false),
diff --git a/cmd/hakurei/command_test.go b/cmd/hakurei/command_test.go
index 059e18b1..49a1f9d4 100644
--- a/cmd/hakurei/command_test.go
+++ b/cmd/hakurei/command_test.go
@@ -39,8 +39,8 @@ commands:
[--dbus-system <value>] [--mpris] [--dbus-log]
[--id <value>] [-a <int>] [-g <value>] [-d <value>]
[-u <value>] [--policy <value>] [--priority <int>]
- [--private-runtime] [--private-tmpdir] [--wayland] [-X]
- [--dbus] [--pipewire] [--pulse] <command> [<args>]
+ [--no-wait] [--private-runtime] [--private-tmpdir]
+ [--wayland] [-X] [--dbus] [--pipewire] [--pulse] <command> [<args>]
flags:
-X Enable direct connection to X11
@@ -62,6 +62,8 @@ flags:
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
-mpris
Allow owning MPRIS D-Bus path, has no effect if custom config is available
+ -no-wait
+ Do not wait for lingering processes to terminate
-pipewire
Enable connection to PipeWire via SecurityContext
-policy string
diff --git a/cmd/hakurei/testsuite/sandbox/main.go b/cmd/hakurei/testsuite/sandbox/main.go
index 88e028e1..a85ec485 100644
--- a/cmd/hakurei/testsuite/sandbox/main.go
+++ b/cmd/hakurei/testsuite/sandbox/main.go
@@ -5,11 +5,8 @@
package main
import (
- "bytes"
"context"
- "encoding/json"
"fmt"
- "io"
"log"
"os"
"os/exec"
@@ -21,31 +18,13 @@ import (
"sync"
"syscall"
- "hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/hst"
- "hakurei.app/internal/store"
"hakurei.app/internal/testsuite"
"hakurei.app/cmd/hakurei/testsuite/sandbox/testdata"
)
-// mustScanFor continuously scans the proc filesystem and calls f for each entry
-// visited.
-func mustScanFor(f func(ps *testsuite.StatScanner) bool) int {
- var ps testsuite.StatScanner
-
- for ps.Scan() {
- if f(&ps) {
- break
- }
- }
- if err := ps.Err(); err != nil {
- log.Fatal(err)
- }
- return ps.Stat().PID
-}
-
// mustStart starts a hakurei container and returns the pid of a process within
// the container. This process must be terminated by the caller.
func mustStart(
@@ -56,14 +35,14 @@ func mustStart(
) (pid int, done <-chan error) {
_serial := strconv.FormatUint(serial, 10)
_, done = testsuite.MustStartWith(
- ctx, cred, nil, files,
+ ctx, cred, false, nil, files,
"hakurei", "exec",
"-a", strconv.Itoa(identity),
"sleep", "infinity", _serial,
)
var stat syscall.Stat_t
- pid = mustScanFor(func(s *testsuite.StatScanner) bool {
+ pid = testsuite.MustScanFor(func(s *testsuite.StatScanner) bool {
select {
case err := <-done:
if err == nil {
@@ -223,12 +202,12 @@ func main() {
for _, p := range []byte{'a', 'b', 'i', 'p'} {
testsuite.MustFail(
- &cred, nil,
+ &cred, nil, false,
"hakurei", "exec", "capsh", "--has-"+string(p)+"=CAP_SYS_ADMIN",
)
}
testsuite.MustFail(
- &cred, nil,
+ &cred, nil, false,
"hakurei", "exec", "umount", "-R", "/dev",
)
})
@@ -292,12 +271,7 @@ func main() {
}
})
- if err := os.MkdirAll(testsuite.XDGRuntimeDir, 0700); err != nil {
- log.Fatal(err)
- } else if err = os.Chown(testsuite.XDGRuntimeDir, 1000, 1000); err != nil {
- log.Fatal(err)
- }
-
+ testsuite.MustPrepareUserDirs(&cred)
var swg sync.WaitGroup
defer swg.Wait()
dbusEnv := testsuite.MustStartSessionBus(&cred)
@@ -310,156 +284,17 @@ func main() {
}
log.Println("created test helper")
- s := store.New(check.MustAbs("/tmp/hakurei.0/state"))
for name, tc := range testdata.All() {
wg.Go(func() {
- cmd := exec.Command(
- "script", "/dev/null",
- "-E", "always",
- "-qec",
- "hakurei run "+
- "--identifier-fd=5"+
- " 4 1>&3",
- )
- cmd.SysProcAttr = &syscall.SysProcAttr{
- Pdeathsig: syscall.SIGTERM,
- Credential: &cred,
- }
- var output bytes.Buffer
- cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, &output, &output
- cmd.Env = []string{
- "PATH=" + os.Getenv("PATH"),
- "TERM=xterm",
- testsuite.XDGRuntimeEnv,
- testsuite.WaylandEnv,
- "DISPLAY=:0",
- dbusEnv,
- }
-
- var err error
- var notify, _notify, _conf, conf, ident, _ident *os.File
- if notify, _notify, err = os.Pipe(); err != nil {
- log.Fatal(err)
- }
- cmd.ExtraFiles = append(cmd.ExtraFiles, _notify)
- if _conf, conf, err = os.Pipe(); err != nil {
- log.Fatal(err)
- }
- cmd.ExtraFiles = append(cmd.ExtraFiles, _conf)
- if ident, _ident, err = os.Pipe(); err != nil {
- log.Fatal(err)
- }
- cmd.ExtraFiles = append(cmd.ExtraFiles, _ident)
-
- done := testsuite.MustStart(cmd)
- wg.Go(func() {
- _err := <-done
- log.Printf("completed test case %s\n%s", name, output.String())
- if _err != nil {
- log.Fatalf("test case %s: %v", name, _err)
- }
- })
-
- if err = json.NewEncoder(conf).Encode(&tc.Hakurei); err != nil {
- log.Fatal(err)
- } else if err = conf.Close(); err != nil {
- log.Fatal(err)
- }
-
- var id hst.ID
- if _, err = io.ReadFull(ident, id[:]); err != nil {
- log.Fatal(err)
- } else if err = ident.Close(); err != nil {
- log.Fatal(err)
- }
-
- if _, err = io.ReadFull(notify, make([]byte, 8)); err != nil {
- log.Fatal(err)
- } else if err = notify.Close(); err != nil {
- log.Fatal(err)
- }
-
- var (
- ok bool
- p hst.State
- )
- entries, copyError := s.All()
- for entry := range entries {
- if entry.ID == id {
- ok = true
- if _, err = entry.Load(&p, nil); err != nil {
- log.Fatal(err)
- }
- break
- }
- }
- if err = copyError(); err != nil {
- log.Fatal(err)
- }
- if !ok {
- log.Fatalf("instance %s is not present in store", id)
- }
-
- var stat syscall.Stat_t
- pid := mustScanFor(func(ps *testsuite.StatScanner) bool {
- select {
- case err = <-done:
- if err == nil {
- log.Fatal("test process terminated unexpectedly")
- }
- log.Fatal(err)
- default:
- break
- }
-
- if ps.Stat().Comm != "test-helper" {
- return false
- }
-
- var args []string
- if args, err = ps.Stat().Args(); err != nil {
- if testsuite.IsNotExist(err) {
- return false
- }
- log.Fatal(err)
- } else if !slices.Equal(args, tc.Hakurei.Container.Args) {
- return false
- }
-
- if err = ps.Stat().Stat(&stat); err != nil {
- if testsuite.IsNotExist(err) {
- return false
- }
- log.Fatal(err)
- }
-
- uid := hst.ToUser[uint32](0, uint32(tc.Hakurei.Identity))
- if stat.Uid != uid || stat.Gid != uid {
- return false
- }
-
- var t []byte
- if t, err = os.ReadFile(filepath.Join(
- fhs.Proc,
- strconv.Itoa(ps.Stat().PPID),
- "stat",
- )); err != nil {
- if testsuite.IsNotExist(err) {
- return false
+ _, pid := testsuite.MustStartHakurei(
+ ctx, &wg, &cred, dbusEnv,
+ func(err error, output string) {
+ log.Printf("completed test case %s\n%s", name, output)
+ if err != nil {
+ log.Fatalf("test case %s: %v", name, err)
}
- log.Fatal(err)
- }
-
- var _stat testsuite.Stat
- if err = _stat.UnmarshalText(t); err != nil {
- log.Fatal(err)
- }
- if _stat.PPID != p.ShimPID {
- return false
- }
-
- return true
- })
+ }, true, false, &tc.Hakurei,
+ )
testsuite.MustCheckFilter(
pid,
diff --git a/hst/container.go b/hst/container.go
index 824b0982..c4cd2712 100644
--- a/hst/container.go
+++ b/hst/container.go
@@ -141,6 +141,13 @@ type ContainerConfig struct {
// [WaitDelayDefault].
WaitDelay time.Duration `json:"wait_delay,omitempty"`
+ // Duration in nanoseconds to wait for lingering processes after the initial
+ // process terminates.
+ //
+ // Defaults to five seconds if zero or greater than five seconds. Values
+ // lesser than zero is equivalent to zero.
+ AdoptWaitDelay time.Duration `json:"adopt_wait_delay,omitempty"`
+
// Initial process environment variables.
Env map[string]string `json:"env"`
diff --git a/internal/outcome/outcome.go b/internal/outcome/outcome.go
index 414b421f..19ca9c7b 100644
--- a/internal/outcome/outcome.go
+++ b/internal/outcome/outcome.go
@@ -6,6 +6,7 @@ import (
"errors"
"maps"
"strconv"
+ "time"
"hakurei.app/check"
"hakurei.app/container"
@@ -125,6 +126,14 @@ func newOutcomeState(k syscallDispatcher, msg message.Msg, id *hst.ID, config *h
s.Shim.WaitDelay = s.Container.WaitDelay
}
+ if s.Container.AdoptWaitDelay > 5*time.Second {
+ s.Shim.AdoptWaitDelay = 5 * time.Second
+ } else if s.Container.AdoptWaitDelay < 0 {
+ s.Shim.AdoptWaitDelay = -1
+ } else {
+ s.Shim.AdoptWaitDelay = s.Container.AdoptWaitDelay
+ }
+
if s.Container.Flags&hst.FMapRealUID != 0 {
s.Mapuid, s.Mapgid = k.getuid(), k.getgid()
} else {
diff --git a/internal/outcome/shim.go b/internal/outcome/shim.go
index 6e039b65..2e7b0910 100644
--- a/internal/outcome/shim.go
+++ b/internal/outcome/shim.go
@@ -72,6 +72,12 @@ type shimParams struct {
// Limits are enforced on the priv side.
WaitDelay time.Duration
+ // Duration to wait after the initial process terminates before all
+ // lingering processes are killed.
+ //
+ // Limits are enforced on the priv side.
+ AdoptWaitDelay time.Duration
+
// Verbosity pass through from [message.Msg].
Verbose bool
@@ -286,6 +292,7 @@ func shimEntrypoint(k syscallDispatcher) {
// bounds and default enforced in finalise.go
z.WaitDelay = state.Shim.WaitDelay
+ z.AdoptWaitDelay = state.Shim.AdoptWaitDelay
if stateParams.pipewirePulsePath != nil {
zpw := container.NewCommand(ctx, msg, stateParams.pipewirePulsePath, pipewirePulseName)
diff --git a/internal/testsuite/testsuite.go b/internal/testsuite/testsuite.go
index 2c82e1e9..fee4952c 100644
--- a/internal/testsuite/testsuite.go
+++ b/internal/testsuite/testsuite.go
@@ -8,18 +8,27 @@ import (
"bufio"
"context"
"crypto/sha512"
+ "encoding/json"
"errors"
+ "io"
"log"
"os"
"os/exec"
"os/signal"
"os/user"
+ "path/filepath"
+ "slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
+
+ "hakurei.app/check"
+ "hakurei.app/fhs"
+ "hakurei.app/hst"
+ "hakurei.app/internal/store"
)
// serial is the next value returned by [NewSerial].
@@ -101,9 +110,16 @@ var ErrUnexpectedSuccess = errors.New("process unexpectedly exited with code 0")
// MustFail runs command and terminates the testsuite if the program fails to
// start or exits with code 0.
-func MustFail(cred *syscall.Credential, extraEnv []string, command ...string) {
+func MustFail(
+ cred *syscall.Credential,
+ extraEnv []string,
+ quiet bool,
+ command ...string,
+) {
cmd := exec.Command(command[0], command[1:]...)
- cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
+ if !quiet {
+ cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
+ }
cmd.SysProcAttr = &syscall.SysProcAttr{
Pdeathsig: syscall.SIGKILL,
Credential: cred,
@@ -132,12 +148,15 @@ func MustStart(cmd *exec.Cmd) (done <-chan error) {
func MustStartWith(
ctx context.Context,
cred *syscall.Credential,
+ quiet bool,
extraEnv []string,
files []*os.File,
command ...string,
) (proc *os.Process, done <-chan error) {
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
- cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
+ if !quiet {
+ cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
+ }
cmd.ExtraFiles = files
cmd.SysProcAttr = &syscall.SysProcAttr{
Pdeathsig: syscall.SIGTERM,
@@ -147,6 +166,71 @@ func MustStartWith(
return cmd.Process, MustStart(cmd)
}
+// MustOutput is like [MustRun] and [MustFail], but collects the entire output
+// in memory and returns them as strings.
+func MustOutput(
+ ctx context.Context,
+ cred *syscall.Credential,
+ extraEnv []string,
+ exitCode int,
+ name string,
+ command ...string,
+) (string, string) {
+ var stdout, stderr strings.Builder
+ cmd := exec.CommandContext(ctx, command[0], command[1:]...)
+ cmd.Stdout, cmd.Stderr = &stdout, &stderr
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Pdeathsig: syscall.SIGTERM,
+ Credential: cred,
+ }
+ MustAppendEnv(cmd, extraEnv...)
+
+ err := cmd.Run()
+ if err == nil {
+ if exitCode == 0 {
+ return stdout.String(), stderr.String()
+ }
+ log.Fatalf(
+ "%s succeeded unexpectedly"+
+ "\nstdout = %q\nstderr = %q",
+ name,
+ stdout.String(),
+ stderr.String(),
+ )
+ }
+
+ if e, ok := errors.AsType[*exec.ExitError](err); !ok {
+ log.Fatal(err)
+ } else if e.ExitCode() != exitCode {
+ log.Fatalf(
+ "%s: %v"+
+ "\nstdout = %q\nstderr = %q",
+ name, e,
+ stdout.String(),
+ stderr.String(),
+ )
+ }
+ return stdout.String(), stderr.String()
+}
+
+// AssertOutput returns a function that terminates the program if called with
+// arguments differing from the ones it is initialised with.
+func AssertOutput(action, stdout, stderr string) (
+ assert func(stdout, stderr string),
+) {
+ return func(gotStdout, gotStderr string) {
+ if stdout != gotStdout || stderr != gotStderr {
+ log.Fatalf(
+ "unexpected output %s"+
+ "\nstdout = %q\nstderr = %q",
+ action,
+ gotStdout,
+ gotStderr,
+ )
+ }
+ }
+}
+
// MustCheckFilter is like [CheckFilter], but terminates the test suite if a
// non-nil error is returned. Otherwise, the tracee is terminated after it
// resumes.
@@ -202,14 +286,47 @@ func Poll(
}
const (
- // XDGRuntimeDir is the hardcoded XDG runtime directory for the user
- // described by [GetUser].
+ // XDGRuntimeDir is the hardcoded XDG runtime directory of the only user
+ // mapped in hsurc.
XDGRuntimeDir = "/var/run/user/1000"
// XDGRuntimeEnv is the environment variable string for XDG_RUNTIME_DIR.
XDGRuntimeEnv = "XDG_RUNTIME_DIR=" + XDGRuntimeDir
+
+ // HakureiPrefix is the hakurei work directory of the only user mapped in
+ // hsurc.
+ HakureiPrefix = "/tmp/hakurei.0/"
)
+// MustPrepareUserDirs creates user-specific directories.
+func MustPrepareUserDirs(cred *syscall.Credential) {
+ if err := os.MkdirAll(XDGRuntimeDir, 0700); err != nil {
+ log.Fatal(err)
+ } else if err = os.Chown(
+ XDGRuntimeDir,
+ int(cred.Uid),
+ int(cred.Gid),
+ ); err != nil {
+ log.Fatal(err)
+ }
+}
+
+// SessionEnv returns environment variables referring to the hosted session.
+func SessionEnv(dbusEnv string) []string {
+ return []string{
+ "PATH=" + os.Getenv("PATH"),
+ "TERM=xterm",
+ XDGRuntimeEnv,
+ WaylandEnv,
+ "DISPLAY=:0",
+ dbusEnv,
+ }
+}
+
+// quietServices is initialised from the environment and controls whether
+// session services should have their outputs connected.
+var quietServices = os.Getenv("HAKUREI_VERBOSE_SERVICES") == ""
+
// MustStartSessionBus starts a session bus that is never explicitly terminated.
// The test suite is terminated if the session bus daemon terminates.
func MustStartSessionBus(cred *syscall.Credential) (dbusEnv string) {
@@ -220,7 +337,7 @@ func MustStartSessionBus(cred *syscall.Credential) (dbusEnv string) {
// this is never explicitly terminated
_, done := MustStartWith(
- context.Background(), cred, nil, []*os.File{w},
+ context.Background(), cred, quietServices, nil, []*os.File{w},
"dbus-daemon",
"--print-address=3",
"--address=unix:path="+XDGRuntimeDir+"/dbus",
@@ -267,7 +384,7 @@ func MustStartSway(
wg.Go(func() {
// this is terminated via swaymsg
_, done := MustStartWith(
- context.Background(), cred, []string{
+ context.Background(), cred, quietServices, []string{
"WLR_BACKENDS=headless",
XDGRuntimeEnv,
SwayEnv,
@@ -291,7 +408,7 @@ func MustStartSway(
// TerminateSway requests for the sway server to terminate via sway IPC.
func TerminateSway(cred *syscall.Credential) {
- MustFail(cred, []string{SwayEnv}, "swaymsg", "exit")
+ MustFail(cred, []string{SwayEnv}, quietServices, "swaymsg", "exit")
}
// MustStartPipeWire starts a PipeWire server that is never explicitly
@@ -299,7 +416,7 @@ func TerminateSway(cred *syscall.Credential) {
func MustStartPipeWire(cred *syscall.Credential, dbusEnv string) {
// this is never explicitly terminated
_, done := MustStartWith(
- context.Background(), cred, []string{
+ context.Background(), cred, quietServices, []string{
XDGRuntimeEnv,
dbusEnv,
}, nil,
@@ -322,7 +439,7 @@ func MustStartPipeWire(cred *syscall.Credential, dbusEnv string) {
)
_, _done := MustStartWith(
- context.Background(), cred, []string{
+ context.Background(), cred, quietServices, []string{
XDGRuntimeEnv,
dbusEnv,
}, nil,
@@ -336,3 +453,227 @@ func MustStartPipeWire(cred *syscall.Credential, dbusEnv string) {
log.Fatal("wireplumber terminated unexpectedly")
}()
}
+
+// MustScanFor continuously scans the proc filesystem and calls f for each entry
+// visited.
+func MustScanFor(f func(ps *StatScanner) bool) int {
+ var ps StatScanner
+
+ for ps.Scan() {
+ if f(&ps) {
+ break
+ }
+ }
+ if err := ps.Err(); err != nil {
+ log.Fatal(err)
+ }
+ return ps.Stat().PID
+}
+
+// StartHakurei starts a hakurei container configured by c.
+func StartHakurei(
+ ctx context.Context,
+ wg *sync.WaitGroup,
+ cred *syscall.Credential,
+ dbusEnv string,
+ processCommand func(cmd *exec.Cmd),
+ onDone func(err error, output string),
+ scan, wait, verbose bool,
+ c *hst.Config,
+) (p hst.State, pid int, err error) {
+ s := store.New(check.MustAbs("/tmp/hakurei.0/state"))
+
+ hakureiFlags := ""
+ if verbose {
+ hakureiFlags += " -v"
+ }
+
+ cmd := exec.CommandContext(ctx,
+ "script", "/dev/null",
+ "-E", "always",
+ "-qec",
+ "hakurei"+hakureiFlags+" run "+
+ "--identifier-fd=5"+
+ " 4 1>&3",
+ )
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Pdeathsig: syscall.SIGTERM,
+ Credential: cred,
+ }
+ var output strings.Builder
+ cmd.Stdout, cmd.Stderr = &output, &output
+ cmd.Env = SessionEnv(dbusEnv)
+
+ var notify, _notify, _conf, conf, ident, _ident *os.File
+ if notify, _notify, err = os.Pipe(); err != nil {
+ return
+ }
+ cmd.ExtraFiles = append(cmd.ExtraFiles, _notify)
+ if _conf, conf, err = os.Pipe(); err != nil {
+ _, _ = notify.Close(), _notify.Close()
+ return
+ }
+ cmd.ExtraFiles = append(cmd.ExtraFiles, _conf)
+ if ident, _ident, err = os.Pipe(); err != nil {
+ _, _ = notify.Close(), _notify.Close()
+ _, _ = conf.Close(), _conf.Close()
+ return
+ }
+ cmd.ExtraFiles = append(cmd.ExtraFiles, _ident)
+ if processCommand != nil {
+ processCommand(cmd)
+ }
+
+ done := MustStart(cmd)
+ wg.Go(func() { _err := <-done; onDone(_err, output.String()) })
+
+ if err = errors.Join(
+ _notify.Close(),
+ _conf.Close(),
+ _ident.Close(),
+ ); err != nil {
+ _, _, _ = notify.Close(), conf.Close(), ident.Close()
+ return
+ }
+
+ if err = json.NewEncoder(conf).Encode(c); err != nil {
+ _, _, _ = notify.Close(), conf.Close(), ident.Close()
+ return
+ } else if err = conf.Close(); err != nil {
+ _, _ = notify.Close(), ident.Close()
+ return
+ }
+
+ if !scan && len(cmd.ExtraFiles) != 3 {
+ err = errors.Join(notify.Close(), ident.Close())
+ return
+ }
+
+ var id hst.ID
+ if _, err = io.ReadFull(ident, id[:]); err != nil {
+ _, _ = notify.Close(), ident.Close()
+ return
+ } else if err = ident.Close(); err != nil {
+ _ = notify.Close()
+ return
+ }
+
+ if !scan {
+ err = notify.Close()
+ return
+ }
+
+ if wait {
+ if _, err = io.ReadFull(notify, make([]byte, 8)); err != nil {
+ _ = notify.Close()
+ return
+ }
+ }
+ if err = notify.Close(); err != nil {
+ return
+ }
+
+ var ok bool
+ entries, copyError := s.All()
+ for entry := range entries {
+ if entry.ID == id {
+ ok = true
+ if _, err = entry.Load(&p, nil); err != nil {
+ log.Fatal(err)
+ }
+ break
+ }
+ }
+ if err = copyError(); err != nil {
+ log.Fatal(err)
+ }
+ if !ok {
+ log.Fatalf("instance %s is not present in store", id)
+ }
+
+ var stat syscall.Stat_t
+ pid = MustScanFor(func(ps *StatScanner) bool {
+ select {
+ case err = <-done:
+ if err == nil {
+ log.Fatal("test process terminated unexpectedly")
+ }
+ log.Fatal(err)
+ default:
+ break
+ }
+
+ var args []string
+ if args, err = ps.Stat().Args(); err != nil {
+ if IsNotExist(err) {
+ return false
+ }
+ log.Fatal(err)
+ } else if !slices.Equal(args, c.Container.Args) {
+ return false
+ }
+
+ if err = ps.Stat().Stat(&stat); err != nil {
+ if IsNotExist(err) {
+ return false
+ }
+ log.Fatal(err)
+ }
+
+ uid := hst.ToUser[uint32](0, uint32(c.Identity))
+ if stat.Uid != uid || stat.Gid != uid {
+ return false
+ }
+
+ var t []byte
+ if t, err = os.ReadFile(filepath.Join(
+ fhs.Proc,
+ strconv.Itoa(ps.Stat().PPID),
+ "stat",
+ )); err != nil {
+ if IsNotExist(err) {
+ return false
+ }
+ log.Fatal(err)
+ }
+
+ var _stat Stat
+ if err = _stat.UnmarshalText(t); err != nil {
+ log.Fatal(err)
+ }
+ if _stat.PPID != p.ShimPID {
+ return false
+ }
+
+ return true
+ })
+ return
+}
+
+// MustStartHakurei is like [StartHakurei], but terminates the test suite if
+// an error is encountered.
+func MustStartHakurei(
+ ctx context.Context,
+ wg *sync.WaitGroup,
+ cred *syscall.Credential,
+ dbusEnv string,
+ onDone func(err error, output string),
+ wait, verbose bool,
+ c *hst.Config,
+) (p hst.State, pid int) {
+ _ctx, cancel := context.WithCancel(ctx)
+
+ var err error
+ p, pid, err = StartHakurei(
+ _ctx, wg, cred, dbusEnv,
+ nil, func(err error, output string) {
+ defer cancel()
+ onDone(err, output)
+ }, true, wait, verbose, c,
+ )
+ if err != nil {
+ log.Printf("cannot start hakurei container: %v", err)
+ cancel()
+ }
+ return
+}
diff --git a/internal/wayland/security-context-v1-protocol.c b/internal/wayland/security-context-v1-protocol.c
index 5b15b1b4..54cd69d2 100644
--- a/internal/wayland/security-context-v1-protocol.c
+++ b/internal/wayland/security-context-v1-protocol.c
@@ -1,4 +1,4 @@
-/* Generated by wayland-scanner 1.23.1 */
+/* Generated by wayland-scanner 1.26.0 */
/*
* Copyright © 2021 Simon Ser
diff --git a/internal/wayland/security-context-v1-protocol.h b/internal/wayland/security-context-v1-protocol.h
index f973619e..0b5bc69e 100644
--- a/internal/wayland/security-context-v1-protocol.h
+++ b/internal/wayland/security-context-v1-protocol.h
@@ -1,4 +1,4 @@
-/* Generated by wayland-scanner 1.23.1 */
+/* Generated by wayland-scanner 1.26.0 */
#ifndef SECURITY_CONTEXT_V1_CLIENT_PROTOCOL_H
#define SECURITY_CONTEXT_V1_CLIENT_PROTOCOL_H