Compare commits

..

13 Commits

Author SHA1 Message Date
cb513bb1cd
release: 0.1.2
All checks were successful
Release / Create release (push) Successful in 41s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (push) Successful in 2m37s
Test / Create distribution (push) Successful in 24s
Test / Sandbox (race detector) (push) Successful in 3m29s
Test / Planterette (push) Successful in 3m5s
Test / Hakurei (race detector) (push) Successful in 2m27s
Test / Flake checks (push) Successful in 1m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 03:11:33 +09:00
f7bd28118c
hst: configurable wait delay
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m58s
Test / Hakurei (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Planterette (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m17s
This is useful for programs that take a long time to clean up.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 03:06:49 +09:00
940ee00ffe
container/init: configurable lingering process wait delay
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m57s
Test / Hakurei (push) Successful in 2m50s
Test / Planterette (push) Successful in 3m39s
Test / Sandbox (race detector) (push) Successful in 3m43s
Test / Hakurei (race detector) (push) Successful in 4m33s
Test / Flake checks (push) Successful in 1m16s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 02:38:17 +09:00
b43d104680
app: integrate interrupt forwarding
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m58s
Test / Hakurei (push) Successful in 2m53s
Test / Sandbox (race detector) (push) Successful in 3m53s
Test / Planterette (push) Successful in 3m53s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m19s
This significantly increases usability of command line tools running through hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 02:23:06 +09:00
ddf48a6c22
app/shim: implement signal handler outcome in Go
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m53s
Test / Hakurei (push) Successful in 2m48s
Test / Planterette (push) Successful in 3m48s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hakurei (race detector) (push) Successful in 4m27s
Test / Flake checks (push) Successful in 1m13s
This needs to be done from the Go side eventually anyway to integrate the signal forwarding behaviour now supported by the container package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-28 23:39:30 +09:00
a0f499e30a
app/shim: separate signal handler implementation
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m57s
Test / Planterette (push) Successful in 3m44s
Test / Sandbox (race detector) (push) Successful in 3m50s
Test / Hakurei (race detector) (push) Successful in 4m25s
Test / Hakurei (push) Successful in 2m0s
Test / Flake checks (push) Successful in 1m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-28 21:52:53 +09:00
d6b07f12ff
container: forward context cancellation
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m56s
Test / Hakurei (push) Successful in 2m47s
Test / Planterette (push) Successful in 3m40s
Test / Sandbox (race detector) (push) Successful in 3m45s
Test / Hakurei (race detector) (push) Successful in 4m29s
Test / Flake checks (push) Successful in 1m18s
This allows container processes to exit gracefully.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-28 01:45:38 +09:00
65fe09caf9
container: check cancel signal delivery
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m50s
Test / Sandbox (race detector) (push) Successful in 3m46s
Test / Planterette (push) Successful in 3m52s
Test / Hakurei (race detector) (push) Successful in 4m28s
Test / Flake checks (push) Successful in 1m18s
This change also makes some parts of the test more robust.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-28 01:04:29 +09:00
a1e5f020f4
container: improve doc comments
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 2m3s
Test / Hakurei (push) Successful in 2m53s
Test / Sandbox (race detector) (push) Successful in 3m43s
Test / Planterette (push) Successful in 3m57s
Test / Hakurei (race detector) (push) Successful in 4m23s
Test / Flake checks (push) Successful in 1m10s
Putting them on the builder methods is more useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-27 12:27:42 +09:00
bd3fa53a55
container: access test case by index in helper
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Hakurei (push) Successful in 40s
Test / Sandbox (push) Successful in 38s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Planterette (push) Successful in 39s
Test / Flake checks (push) Successful in 1m17s
This is more elegant and allows for much easier extension of the tests. Mountinfo is still serialised however due to libPaths nondeterminism.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-26 18:59:19 +09:00
625632c593
nix: update flake lock
All checks were successful
Test / Create distribution (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 50s
Test / Sandbox (push) Successful in 52s
Test / Planterette (push) Successful in 50s
Test / Hakurei (race detector) (push) Successful in 57s
Test / Hakurei (push) Successful in 59s
Test / Flake checks (push) Successful in 1m53s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-26 18:57:54 +09:00
e71ae3b8c5
container: remove custom cmd initialisation
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Hakurei (push) Successful in 45s
Test / Sandbox (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Sandbox (race detector) (push) Successful in 43s
Test / Planterette (push) Successful in 43s
Test / Flake checks (push) Successful in 1m27s
This part of the interface is very unintuitive and only used for testing, even in testing it is inelegant and can be done better.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-25 00:45:10 +09:00
9d7a19d162
container: use more reliable nonexistence
All checks were successful
Test / Create distribution (push) Successful in 45s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m41s
Test / Flake checks (push) Successful in 1m18s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-18 23:18:26 +09:00
45 changed files with 803 additions and 519 deletions

View File

@ -256,8 +256,10 @@ App
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
@ -382,8 +384,10 @@ App
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
@ -562,8 +566,10 @@ func Test_printPs(t *testing.T) {
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,

View File

@ -215,15 +215,14 @@ stdenv.mkDerivation {
# create binary cache # create binary cache
closureInfo="${ closureInfo="${
closureInfo { closureInfo {
rootPaths = rootPaths = [
[ homeManagerConfiguration.activationPackage
homeManagerConfiguration.activationPackage launcher
launcher ]
] ++ optionals gpu [
++ optionals gpu [ mesaWrappers
mesaWrappers nixGL
nixGL ];
];
} }
}" }"
echo "copying application paths..." echo "copying application paths..."

View File

@ -17,6 +17,16 @@ import (
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
) )
const (
// Nonexistent is a path that cannot exist.
// /proc is chosen because a system with covered /proc is unsupported by this package.
Nonexistent = "/proc/nonexistent"
// CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGTERM
)
type ( type (
// Container represents a container environment being prepared or run. // Container represents a container environment being prepared or run.
// None of [Container] methods are safe for concurrent use. // None of [Container] methods are safe for concurrent use.
@ -29,9 +39,6 @@ type (
// with behaviour identical to its [exec.Cmd] counterpart. // with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File ExtraFiles []*os.File
// Custom [exec.Cmd] initialisation function.
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
// param encoder for shim and init // param encoder for shim and init
setup *gob.Encoder setup *gob.Encoder
// cancels cmd // cancels cmd
@ -59,6 +66,10 @@ type (
Path string Path string
// Initial process argv. // Initial process argv.
Args []string Args []string
// Deliver SIGINT to the initial process on context cancellation.
ForwardCancel bool
// time to wait for linger processes after death of initial process
AdoptWaitDelay time.Duration
// Mapped Uid in user namespace. // Mapped Uid in user namespace.
Uid int Uid int
@ -68,6 +79,7 @@ type (
Hostname string Hostname string
// Sequential container setup ops. // Sequential container setup ops.
*Ops *Ops
// Seccomp system call filter rules. // Seccomp system call filter rules.
SeccompRules []seccomp.NativeRule SeccompRules []seccomp.NativeRule
// Extra seccomp flags. // Extra seccomp flags.
@ -76,6 +88,7 @@ type (
SeccompPresets seccomp.FilterPreset SeccompPresets seccomp.FilterPreset
// Do not load seccomp program. // Do not load seccomp program.
SeccompDisable bool SeccompDisable bool
// Permission bits of newly created parent directories. // Permission bits of newly created parent directories.
// The zero value is interpreted as 0755. // The zero value is interpreted as 0755.
ParentPerm os.FileMode ParentPerm os.FileMode
@ -88,12 +101,13 @@ type (
} }
) )
// Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error { func (p *Container) Start() error {
if p.cmd != nil { if p.cmd != nil {
return errors.New("sandbox: already started") return errors.New("container: already started")
} }
if p.Ops == nil || len(*p.Ops) == 0 { if p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("sandbox: starting an empty container") return errors.New("container: starting an empty container")
} }
ctx, cancel := context.WithCancel(p.ctx) ctx, cancel := context.WithCancel(p.ctx)
@ -116,19 +130,22 @@ func (p *Container) Start() error {
p.SeccompPresets |= seccomp.PresetDenyTTY p.SeccompPresets |= seccomp.PresetDenyTTY
} }
if p.CommandContext != nil { if p.AdoptWaitDelay == 0 {
p.cmd = p.CommandContext(ctx) p.AdoptWaitDelay = 5 * time.Second
} else { }
p.cmd = exec.CommandContext(ctx, MustExecutable()) // to allow disabling this behaviour
p.cmd.Args = []string{"init"} if p.AdoptWaitDelay < 0 {
p.AdoptWaitDelay = 0
} }
p.cmd = exec.CommandContext(ctx, MustExecutable())
p.cmd.Args = []string{initName}
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil { if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) } p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
} else { } else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(SIGTERM) } p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
} }
p.cmd.Dir = "/" p.cmd.Dir = "/"
p.cmd.SysProcAttr = &SysProcAttr{ p.cmd.SysProcAttr = &SysProcAttr{
@ -162,6 +179,8 @@ func (p *Container) Start() error {
return nil return nil
} }
// Serve serves [Container.Params] to the container init.
// Serve must only be called once.
func (p *Container) Serve() error { func (p *Container) Serve() error {
if p.setup == nil { if p.setup == nil {
panic("invalid serve") panic("invalid serve")
@ -215,6 +234,7 @@ func (p *Container) Serve() error {
return err return err
} }
// Wait waits for the container init process to exit.
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() } func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
func (p *Container) String() string { func (p *Container) String() string {
@ -222,6 +242,14 @@ func (p *Container) String() string {
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets)) p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
} }
// ProcessState returns the address to os.ProcessState held by the underlying [exec.Cmd].
func (p *Container) ProcessState() *os.ProcessState {
if p.cmd == nil {
return nil
}
return p.cmd.ProcessState
}
func New(ctx context.Context, name string, args ...string) *Container { func New(ctx context.Context, name string, args ...string) *Container {
return &Container{name: name, ctx: ctx, return &Container{name: name, ctx: ctx,
Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)}, Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},

View File

@ -4,28 +4,85 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/gob" "encoding/gob"
"errors"
"fmt"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"strconv"
"strings" "strings"
"syscall" "syscall"
"testing" "testing"
"time"
"hakurei.app/command"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
"hakurei.app/ldd"
) )
const ( const (
ignore = "\x00" ignore = "\x00"
ignoreV = -1 ignoreV = -1
pathWantMnt = "/etc/hakurei/want-mnt"
) )
var containerTestCases = []struct {
name string
filter bool
session bool
net bool
ops *container.Ops
mnt []*vfs.MountInfoEntry
uid int
gid int
rules []seccomp.NativeRule
flags seccomp.ExportFlag
presets seccomp.FilterPreset
}{
{"minimal", true, false, false,
new(container.Ops), nil,
1000, 100, nil, 0, seccomp.PresetStrict},
{"allow", true, true, true,
new(container.Ops), nil,
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
{"no filter", false, true, true,
new(container.Ops), nil,
1000, 100, nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true,
new(container.Ops), nil,
1, 31, []seccomp.NativeRule{{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}}, 0, seccomp.PresetExt},
{"tmpfs", true, false, false,
new(container.Ops).
Tmpfs(hst.Tmp, 0, 0755),
[]*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
},
9, 9, nil, 0, seccomp.PresetStrict},
{"dev", true, true /* go test output is not a tty */, false,
new(container.Ops).
Dev("/dev").
Mqueue("/dev/mqueue"),
[]*vfs.MountInfoEntry{
ent("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
},
1971, 100, nil, 0, seccomp.PresetStrict},
}
func TestContainer(t *testing.T) { func TestContainer(t *testing.T) {
{ {
oldVerbose := hlog.Load() oldVerbose := hlog.Load()
@ -35,124 +92,86 @@ func TestContainer(t *testing.T) {
t.Cleanup(func() { container.SetOutput(oldOutput) }) t.Cleanup(func() { container.SetOutput(oldOutput) })
} }
testCases := []struct { t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
name string wantErr := context.Canceled
filter bool wantExitCode := 0
session bool if err := c.Wait(); !errors.Is(err, wantErr) {
net bool hlog.PrintBaseError(err, "wait:")
ops *container.Ops t.Errorf("Wait: error = %v, want %v", err, wantErr)
mnt []*vfs.MountInfoEntry }
host string if ps := c.ProcessState(); ps == nil {
rules []seccomp.NativeRule t.Errorf("ProcessState unexpectedly returned nil")
flags seccomp.ExportFlag } else if code := ps.ExitCode(); code != wantExitCode {
presets seccomp.FilterPreset t.Errorf("ExitCode: %d, want %d", code, wantExitCode)
}{ }
{"minimal", true, false, false, }))
new(container.Ops), nil, "test-minimal",
nil, 0, seccomp.PresetStrict},
{"allow", true, true, true,
new(container.Ops), nil, "test-minimal",
nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
{"no filter", false, true, true,
new(container.Ops), nil, "test-no-filter",
nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true,
new(container.Ops), nil, "test-no-filter",
[]seccomp.NativeRule{
{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil},
}, 0, seccomp.PresetExt},
{"tmpfs", true, false, false,
new(container.Ops).
Tmpfs(hst.Tmp, 0, 0755),
[]*vfs.MountInfoEntry{
e("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
}, "test-tmpfs",
nil, 0, seccomp.PresetStrict},
{"dev", true, true /* go test output is not a tty */, false,
new(container.Ops).
Dev("/dev").
Mqueue("/dev/mqueue"),
[]*vfs.MountInfoEntry{
e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
}, "",
nil, 0, seccomp.PresetStrict},
}
for _, tc := range testCases { t.Run("forward", testContainerCancel(func(c *container.Container) {
c.ForwardCancel = true
}, func(t *testing.T, c *container.Container) {
var exitError *exec.ExitError
if err := c.Wait(); !errors.As(err, &exitError) {
hlog.PrintBaseError(err, "wait:")
t.Errorf("Wait: error = %v", err)
}
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
}
}))
for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel() defer cancel()
c := container.New(ctx, "/usr/bin/sandbox.test", "-test.v", var libPaths []string
"-test.run=TestHelperCheckContainer", "--", "check", tc.host) c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = 1000 c.Uid = tc.uid
c.Gid = 100 c.Gid = tc.gid
c.Hostname = tc.host c.Hostname = hostnameFromTestCase(tc.name)
c.CommandContext = commandContext
c.Stdout, c.Stderr = os.Stdout, os.Stderr c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.Ops = tc.ops c.WaitDelay = helperDefaultTimeout
*c.Ops = append(*c.Ops, *tc.ops...)
c.SeccompRules = tc.rules c.SeccompRules = tc.rules
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
c.SeccompPresets = tc.presets c.SeccompPresets = tc.presets
c.SeccompDisable = !tc.filter c.SeccompDisable = !tc.filter
c.RetainSession = tc.session c.RetainSession = tc.session
c.HostNet = tc.net c.HostNet = tc.net
if c.Args[5] == "" {
if name, err := os.Hostname(); err != nil {
t.Fatalf("cannot get hostname: %v", err)
} else {
c.Args[5] = name
}
}
c. c.
Tmpfs("/tmp", 0, 0755). Tmpfs("/tmp", 0, 0755).
Bind(os.Args[0], os.Args[0], 0). Place("/etc/hostname", []byte(c.Hostname))
Mkdir("/usr/bin", 0755).
Link(os.Args[0], "/usr/bin/sandbox.test").
Place("/etc/hostname", []byte(c.Args[5]))
// in case test has cgo enabled
var libPaths []string
if entries, err := ldd.ExecFilter(ctx,
commandContext,
func(v []byte) []byte {
return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1]
}, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
libPaths = ldd.Path(entries)
}
for _, name := range libPaths {
c.Bind(name, name, 0)
}
// needs /proc to check mountinfo // needs /proc to check mountinfo
c.Proc("/proc") c.Proc("/proc")
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
mnt = append(mnt, tc.mnt...)
mnt = append(mnt, mnt = append(mnt,
e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), ent("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore), // Bind(os.Args[0], helperInnerPath, 0)
e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore), ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
) )
for _, name := range libPaths { for _, name := range libPaths {
mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore)) // Bind(name, name, 0)
mnt = append(mnt, ent(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
} }
mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw")) mnt = append(mnt, tc.mnt...)
mnt = append(mnt,
// Tmpfs("/tmp", 0, 0755)
ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
// Place("/etc/hostname", []byte(hostname))
ent(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
// Proc("/proc")
ent("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"),
// Place(pathWantMnt, want.Bytes())
ent(ignore, pathWantMnt, "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
)
want := new(bytes.Buffer) want := new(bytes.Buffer)
if err := gob.NewEncoder(want).Encode(mnt); err != nil { if err := gob.NewEncoder(want).Encode(mnt); err != nil {
t.Fatalf("cannot serialise expected mount points: %v", err) t.Fatalf("cannot serialise expected mount points: %v", err)
} }
c.Stdin = want c.Place(pathWantMnt, want.Bytes())
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
hlog.PrintBaseError(err, "start:") hlog.PrintBaseError(err, "start:")
@ -169,7 +188,7 @@ func TestContainer(t *testing.T) {
} }
} }
func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry { func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
return &vfs.MountInfoEntry{ return &vfs.MountInfoEntry{
ID: ignoreV, ID: ignoreV,
Parent: ignoreV, Parent: ignoreV,
@ -184,6 +203,50 @@ func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoE
} }
} }
func hostnameFromTestCase(name string) string {
return "test-" + strings.Join(strings.Fields(name), "-")
}
func testContainerCancel(
containerExtra func(c *container.Container),
waitCheck func(t *testing.T, c *container.Container),
) func(t *testing.T) {
return func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block")
c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.WaitDelay = helperDefaultTimeout
if containerExtra != nil {
containerExtra(c)
}
ready := make(chan struct{})
if r, w, err := os.Pipe(); err != nil {
t.Fatalf("cannot pipe: %v", err)
} else {
c.ExtraFiles = append(c.ExtraFiles, w)
go func() {
defer close(ready)
if _, err = r.Read(make([]byte, 1)); err != nil {
panic(err.Error())
}
}()
}
if err := c.Start(); err != nil {
hlog.PrintBaseError(err, "start:")
t.Fatalf("cannot start container: %v", err)
} else if err = c.Serve(); err != nil {
hlog.PrintBaseError(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
}
<-ready
cancel()
waitCheck(t, c)
}
}
func TestContainerString(t *testing.T) { func TestContainerString(t *testing.T) {
c := container.New(t.Context(), "ldd", "/usr/bin/env") c := container.New(t.Context(), "ldd", "/usr/bin/env")
c.SeccompFlags |= seccomp.AllowMultiarch c.SeccompFlags |= seccomp.AllowMultiarch
@ -197,85 +260,109 @@ func TestContainerString(t *testing.T) {
} }
} }
func TestHelperInit(t *testing.T) { const (
if len(os.Args) != 5 || os.Args[4] != "init" { blockExitCodeInterrupt = 2
return )
}
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, internal.InstallOutput)
}
func TestHelperCheckContainer(t *testing.T) { func init() {
if len(os.Args) != 6 || os.Args[4] != "check" { helperCommands = append(helperCommands, func(c command.Command) {
return c.Command("block", command.UsageInternal, func(args []string) error {
} if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
t.Run("user", func(t *testing.T) {
if uid := syscall.Getuid(); uid != 1000 {
t.Errorf("Getuid: %d, want 1000", uid)
}
if gid := syscall.Getgid(); gid != 100 {
t.Errorf("Getgid: %d, want 100", gid)
}
})
t.Run("hostname", func(t *testing.T) {
if name, err := os.Hostname(); err != nil {
t.Fatalf("cannot get hostname: %v", err)
} else if name != os.Args[5] {
t.Errorf("Hostname: %q, want %q", name, os.Args[5])
}
if p, err := os.ReadFile("/etc/hostname"); err != nil {
t.Fatalf("%v", err)
} else if string(p) != os.Args[5] {
t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
}
})
t.Run("mount", func(t *testing.T) {
var mnt []*vfs.MountInfoEntry
if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
t.Fatalf("cannot receive expected mount points: %v", err)
}
var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
t.Fatalf("cannot open mountinfo: %v", err)
} else {
d = vfs.NewMountInfoDecoder(f)
}
i := 0
for cur := range d.Entries() {
if i == len(mnt) {
t.Errorf("got more than %d entries", len(mnt))
break
} }
{
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
}
select {}
})
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags c.Command("container", command.UsageInternal, func(args []string) error {
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime") if len(args) != 1 {
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime") return syscall.EINVAL
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime") }
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime") tc := containerTestCases[0]
if i, err := strconv.Atoi(args[0]); err != nil {
if !cur.EqualWithIgnore(mnt[i], "\x00") { return fmt.Errorf("cannot parse test case index: %v", err)
t.Errorf("[FAIL] %s", cur)
} else { } else {
t.Logf("[ OK ] %s", cur) tc = containerTestCases[i]
} }
i++ if uid := syscall.Getuid(); uid != tc.uid {
} return fmt.Errorf("uid: %d, want %d", uid, tc.uid)
if err := d.Err(); err != nil { }
t.Errorf("cannot parse mountinfo: %v", err) if gid := syscall.Getgid(); gid != tc.gid {
} return fmt.Errorf("gid: %d, want %d", gid, tc.gid)
}
if i != len(mnt) { wantHost := hostnameFromTestCase(tc.name)
t.Errorf("got %d entries, want %d", i, len(mnt)) if host, err := os.Hostname(); err != nil {
} return fmt.Errorf("cannot get hostname: %v", err)
} else if host != wantHost {
return fmt.Errorf("hostname: %q, want %q", host, wantHost)
}
if p, err := os.ReadFile("/etc/hostname"); err != nil {
return fmt.Errorf("cannot read /etc/hostname: %v", err)
} else if string(p) != wantHost {
return fmt.Errorf("/etc/hostname: %q, want %q", string(p), wantHost)
}
{
var fail bool
var mnt []*vfs.MountInfoEntry
if f, err := os.Open(pathWantMnt); err != nil {
return fmt.Errorf("cannot open expected mount points: %v", err)
} else if err = gob.NewDecoder(f).Decode(&mnt); err != nil {
return fmt.Errorf("cannot parse expected mount points: %v", err)
} else if err = f.Close(); err != nil {
return fmt.Errorf("cannot close expected mount points: %v", err)
}
var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
return fmt.Errorf("cannot open mountinfo: %v", err)
} else {
d = vfs.NewMountInfoDecoder(f)
}
i := 0
for cur := range d.Entries() {
if i == len(mnt) {
return fmt.Errorf("got more than %d entries", len(mnt))
}
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") {
fail = true
log.Printf("[FAIL] %s", cur)
} else {
log.Printf("[ OK ] %s", cur)
}
i++
}
if err := d.Err(); err != nil {
return fmt.Errorf("cannot parse mountinfo: %v", err)
}
if i != len(mnt) {
return fmt.Errorf("got %d entries, want %d", i, len(mnt))
}
if fail {
return errors.New("one or more mountinfo entries do not match")
}
}
return nil
})
}) })
} }
func commandContext(ctx context.Context) *exec.Cmd {
return exec.CommandContext(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperInit", "--", "init")
}

View File

@ -17,9 +17,6 @@ import (
) )
const ( const (
// time to wait for linger processes after death of initial process
residualProcessTimeout = 5 * time.Second
/* intermediate tmpfs mount point /* intermediate tmpfs mount point
this path might seem like a weird choice, however there are many good reasons to use it: this path might seem like a weird choice, however there are many good reasons to use it:
@ -270,13 +267,14 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
cmd.ExtraFiles = extraFiles cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir cmd.Dir = params.Dir
msg.Verbosef("starting initial program %s", params.Path)
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
log.Fatalf("%v", err) log.Fatalf("%v", err)
} }
msg.Suspend() msg.Suspend()
if err := closeSetup(); err != nil { if err := closeSetup(); err != nil {
log.Println("cannot close setup pipe:", err) log.Printf("cannot close setup pipe: %v", err)
// not fatal // not fatal
} }
@ -310,7 +308,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
} }
if !errors.Is(err, ECHILD) { if !errors.Is(err, ECHILD) {
log.Println("unexpected wait4 response:", err) log.Printf("unexpected wait4 response: %v", err)
} }
close(done) close(done)
@ -318,7 +316,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
// handle signals to dump withheld messages // handle signals to dump withheld messages
sig := make(chan os.Signal, 2) sig := make(chan os.Signal, 2)
signal.Notify(sig, SIGINT, SIGTERM) signal.Notify(sig, os.Interrupt, CancelSignal)
// closed after residualProcessTimeout has elapsed after initial process death // closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{}) timeout := make(chan struct{})
@ -328,9 +326,16 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
select { select {
case s := <-sig: case s := <-sig:
if msg.Resume() { if msg.Resume() {
msg.Verbosef("terminating on %s after process start", s.String()) msg.Verbosef("%s after process start", s.String())
} else { } else {
msg.Verbosef("terminating on %s", s.String()) msg.Verbosef("got %s", s.String())
}
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
msg.Verbose("forwarding context cancellation")
if err := cmd.Process.Signal(os.Interrupt); err != nil {
log.Printf("cannot forward cancellation: %v", err)
}
continue
} }
os.Exit(0) os.Exit(0)
case w := <-info: case w := <-info:
@ -350,10 +355,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
msg.Verbosef("initial process exited with status %#x", w.wstatus) msg.Verbosef("initial process exited with status %#x", w.wstatus)
} }
go func() { go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
time.Sleep(residualProcessTimeout)
close(timeout)
}()
} }
case <-done: case <-done:
msg.BeforeExit() msg.BeforeExit()
@ -366,9 +368,11 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
} }
const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init". // TryArgv0 calls [Init] if the last element of argv0 is "init".
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) { func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" { if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
msg = v msg = v
Init(prepare, setVerbose) Init(prepare, setVerbose)
msg.BeforeExit() msg.BeforeExit()

69
container/init_test.go Normal file
View File

@ -0,0 +1,69 @@
package container_test
import (
"context"
"log"
"os"
"testing"
"time"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
)
const (
envDoCheck = "HAKUREI_TEST_DO_CHECK"
helperDefaultTimeout = 5 * time.Second
helperInnerPath = "/usr/bin/helper"
)
var helperCommands []func(c command.Command)
func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
if os.Getenv(envDoCheck) == "1" {
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
log.SetFlags(0)
log.SetPrefix("helper: ")
return nil
})
for _, f := range helperCommands {
f(c)
}
c.MustParse(os.Args[1:], func(err error) {
if err != nil {
log.Fatal(err.Error())
}
})
return
}
os.Exit(m.Run())
}
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]string, args ...string) (c *container.Container) {
c = container.New(ctx, helperInnerPath, args...)
c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(os.Args[0], helperInnerPath, 0)
// in case test has cgo enabled
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
*libPaths = ldd.Path(entries)
}
for _, name := range *libPaths {
c.Bind(name, name, 0)
}
return
}
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
return helperNewContainerLibPaths(ctx, new([]string), args...)
}

View File

@ -15,7 +15,10 @@ import (
type ( type (
Ops []Op Ops []Op
Op interface {
// Op is a generic setup step ran inside the container init.
// Implementations of this interface are sent as a stream of gobs.
Op interface {
// early is called in host root. // early is called in host root.
early(params *Params) error early(params *Params) error
// apply is called in intermediate root. // apply is called in intermediate root.
@ -27,11 +30,17 @@ type (
} }
) )
// Grow grows the slice Ops points to using [slices.Grow].
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) } func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
func init() { gob.Register(new(BindMountOp)) } func init() { gob.Register(new(BindMountOp)) }
// BindMountOp bind mounts host path Source on container path Target. // Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target].
func (f *Ops) Bind(source, target string, flags int) *Ops {
*f = append(*f, &BindMountOp{source, "", target, flags})
return f
}
type BindMountOp struct { type BindMountOp struct {
Source, SourceFinal, Target string Source, SourceFinal, Target string
@ -39,8 +48,11 @@ type BindMountOp struct {
} }
const ( const (
// BindOptional skips nonexistent host paths.
BindOptional = 1 << iota BindOptional = 1 << iota
// BindWritable mounts filesystem read-write.
BindWritable BindWritable
// BindDevice allows access to devices (special files) on this filesystem.
BindDevice BindDevice
) )
@ -108,14 +120,15 @@ func (b *BindMountOp) String() string {
} }
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable) return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
} }
func (f *Ops) Bind(source, target string, flags int) *Ops {
*f = append(*f, &BindMountOp{source, "", target, flags})
return f
}
func init() { gob.Register(new(MountProcOp)) } func init() { gob.Register(new(MountProcOp)) }
// MountProcOp mounts a private instance of proc. // Proc appends an [Op] that mounts a private instance of proc.
func (f *Ops) Proc(dest string) *Ops {
*f = append(*f, MountProcOp(dest))
return f
}
type MountProcOp string type MountProcOp string
func (p MountProcOp) early(*Params) error { return nil } func (p MountProcOp) early(*Params) error { return nil }
@ -137,14 +150,15 @@ func (p MountProcOp) apply(params *Params) error {
func (p MountProcOp) Is(op Op) bool { vp, ok := op.(MountProcOp); return ok && p == vp } func (p MountProcOp) Is(op Op) bool { vp, ok := op.(MountProcOp); return ok && p == vp }
func (MountProcOp) prefix() string { return "mounting" } func (MountProcOp) prefix() string { return "mounting" }
func (p MountProcOp) String() string { return fmt.Sprintf("proc on %q", string(p)) } func (p MountProcOp) String() string { return fmt.Sprintf("proc on %q", string(p)) }
func (f *Ops) Proc(dest string) *Ops {
*f = append(*f, MountProcOp(dest))
return f
}
func init() { gob.Register(new(MountDevOp)) } func init() { gob.Register(new(MountDevOp)) }
// MountDevOp mounts part of host dev. // Dev appends an [Op] that mounts a subset of host /dev.
func (f *Ops) Dev(dest string) *Ops {
*f = append(*f, MountDevOp(dest))
return f
}
type MountDevOp string type MountDevOp string
func (d MountDevOp) early(*Params) error { return nil } func (d MountDevOp) early(*Params) error { return nil }
@ -231,14 +245,15 @@ func (d MountDevOp) apply(params *Params) error {
func (d MountDevOp) Is(op Op) bool { vd, ok := op.(MountDevOp); return ok && d == vd } func (d MountDevOp) Is(op Op) bool { vd, ok := op.(MountDevOp); return ok && d == vd }
func (MountDevOp) prefix() string { return "mounting" } func (MountDevOp) prefix() string { return "mounting" }
func (d MountDevOp) String() string { return fmt.Sprintf("dev on %q", string(d)) } func (d MountDevOp) String() string { return fmt.Sprintf("dev on %q", string(d)) }
func (f *Ops) Dev(dest string) *Ops {
*f = append(*f, MountDevOp(dest))
return f
}
func init() { gob.Register(new(MountMqueueOp)) } func init() { gob.Register(new(MountMqueueOp)) }
// MountMqueueOp mounts a private mqueue instance on container Path. // Mqueue appends an [Op] that mounts a private instance of mqueue.
func (f *Ops) Mqueue(dest string) *Ops {
*f = append(*f, MountMqueueOp(dest))
return f
}
type MountMqueueOp string type MountMqueueOp string
func (m MountMqueueOp) early(*Params) error { return nil } func (m MountMqueueOp) early(*Params) error { return nil }
@ -260,14 +275,15 @@ func (m MountMqueueOp) apply(params *Params) error {
func (m MountMqueueOp) Is(op Op) bool { vm, ok := op.(MountMqueueOp); return ok && m == vm } func (m MountMqueueOp) Is(op Op) bool { vm, ok := op.(MountMqueueOp); return ok && m == vm }
func (MountMqueueOp) prefix() string { return "mounting" } func (MountMqueueOp) prefix() string { return "mounting" }
func (m MountMqueueOp) String() string { return fmt.Sprintf("mqueue on %q", string(m)) } func (m MountMqueueOp) String() string { return fmt.Sprintf("mqueue on %q", string(m)) }
func (f *Ops) Mqueue(dest string) *Ops {
*f = append(*f, MountMqueueOp(dest))
return f
}
func init() { gob.Register(new(MountTmpfsOp)) } func init() { gob.Register(new(MountTmpfsOp)) }
// MountTmpfsOp mounts tmpfs on container Path. // Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{dest, size, perm})
return f
}
type MountTmpfsOp struct { type MountTmpfsOp struct {
Path string Path string
Size int Size int
@ -288,14 +304,15 @@ func (t *MountTmpfsOp) apply(*Params) error {
func (t *MountTmpfsOp) Is(op Op) bool { vt, ok := op.(*MountTmpfsOp); return ok && *t == *vt } func (t *MountTmpfsOp) Is(op Op) bool { vt, ok := op.(*MountTmpfsOp); return ok && *t == *vt }
func (*MountTmpfsOp) prefix() string { return "mounting" } func (*MountTmpfsOp) prefix() string { return "mounting" }
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) } func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{dest, size, perm})
return f
}
func init() { gob.Register(new(SymlinkOp)) } func init() { gob.Register(new(SymlinkOp)) }
// SymlinkOp creates a symlink in the container filesystem. // Link appends an [Op] that creates a symlink in the container filesystem.
func (f *Ops) Link(target, linkName string) *Ops {
*f = append(*f, &SymlinkOp{target, linkName})
return f
}
type SymlinkOp [2]string type SymlinkOp [2]string
func (l *SymlinkOp) early(*Params) error { func (l *SymlinkOp) early(*Params) error {
@ -331,14 +348,15 @@ func (l *SymlinkOp) apply(params *Params) error {
func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl } func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl }
func (*SymlinkOp) prefix() string { return "creating" } func (*SymlinkOp) prefix() string { return "creating" }
func (l *SymlinkOp) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) } func (l *SymlinkOp) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) }
func (f *Ops) Link(target, linkName string) *Ops {
*f = append(*f, &SymlinkOp{target, linkName})
return f
}
func init() { gob.Register(new(MkdirOp)) } func init() { gob.Register(new(MkdirOp)) }
// MkdirOp creates a directory in the container filesystem. // Mkdir appends an [Op] that creates a directory in the container filesystem.
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{dest, perm})
return f
}
type MkdirOp struct { type MkdirOp struct {
Path string Path string
Perm os.FileMode Perm os.FileMode
@ -359,14 +377,21 @@ func (m *MkdirOp) apply(*Params) error {
func (m *MkdirOp) Is(op Op) bool { vm, ok := op.(*MkdirOp); return ok && m == vm } func (m *MkdirOp) Is(op Op) bool { vm, ok := op.(*MkdirOp); return ok && m == vm }
func (*MkdirOp) prefix() string { return "creating" } func (*MkdirOp) prefix() string { return "creating" }
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) } func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{dest, perm})
return f
}
func init() { gob.Register(new(TmpfileOp)) } func init() { gob.Register(new(TmpfileOp)) }
// TmpfileOp places a file in container Path containing Data. // Place appends an [Op] that places a file in container path [TmpfileOp.Path] containing [TmpfileOp.Data].
func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &TmpfileOp{name, data}); return f }
// PlaceP is like Place but writes the address of [TmpfileOp.Data] to the pointer dataP points to.
func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
t := &TmpfileOp{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}
type TmpfileOp struct { type TmpfileOp struct {
Path string Path string
Data []byte Data []byte
@ -415,19 +440,19 @@ func (*TmpfileOp) prefix() string { return "placing" }
func (t *TmpfileOp) String() string { func (t *TmpfileOp) String() string {
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data)) return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
} }
func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &TmpfileOp{name, data}); return f }
func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
t := &TmpfileOp{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}
func init() { gob.Register(new(AutoEtcOp)) } func init() { gob.Register(new(AutoEtcOp)) }
// AutoEtcOp expands host /etc into a toplevel symlink mirror with /etc semantics. // Etc appends an [Op] that expands host /etc into a toplevel symlink mirror with /etc semantics.
// This is not a generic setup op. It is implemented here to reduce ipc overhead. // This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Etc(host, prefix string) *Ops {
e := &AutoEtcOp{prefix}
f.Mkdir("/etc", 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)
return f
}
type AutoEtcOp struct{ Prefix string } type AutoEtcOp struct{ Prefix string }
func (e *AutoEtcOp) early(*Params) error { return nil } func (e *AutoEtcOp) early(*Params) error { return nil }
@ -473,10 +498,3 @@ func (e *AutoEtcOp) Is(op Op) bool {
} }
func (*AutoEtcOp) prefix() string { return "setting up" } func (*AutoEtcOp) prefix() string { return "setting up" }
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) } func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
func (f *Ops) Etc(host, prefix string) *Ops {
e := &AutoEtcOp{prefix}
f.Mkdir("/etc", 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)
return f
}

View File

@ -79,7 +79,7 @@ func TestExport(t *testing.T) {
func BenchmarkExport(b *testing.B) { func BenchmarkExport(b *testing.B) {
buf := make([]byte, 8) buf := make([]byte, 8)
for i := 0; i < b.N; i++ { for b.Loop() {
e := New( e := New(
Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32, Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32,
AllowMultiarch|AllowCAN|AllowBluetooth), AllowMultiarch|AllowCAN|AllowBluetooth),

12
flake.lock generated
View File

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1748665073, "lastModified": 1753479839,
"narHash": "sha256-RMhjnPKWtCoIIHiuR9QKD7xfsKb3agxzMfJY8V9MOew=", "narHash": "sha256-E/rPVh7vyPMJUFl2NAew+zibNGfVbANr8BP8nLRbLkQ=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "282e1e029cb6ab4811114fc85110613d72771dea", "rev": "0b9bf983db4d064764084cd6748efb1ab8297d1e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -23,11 +23,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1749024892, "lastModified": 1753345091,
"narHash": "sha256-OGcDEz60TXQC+gVz5sdtgGJdKVYr6rwdzQKuZAJQpCA=", "narHash": "sha256-CdX2Rtvp5I8HGu9swBmYuq+ILwRxpXdJwlpg8jvN4tU=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "8f1b52b04f2cb6e5ead50bd28d76528a2f0380ef", "rev": "3ff0e34b1383648053bba8ed03f201d3466f90c9",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -8,12 +8,13 @@ import (
"os/exec" "os/exec"
"testing" "testing"
"hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
) )
func TestCmd(t *testing.T) { func TestCmd(t *testing.T) {
t.Run("start non-existent helper path", func(t *testing.T) { t.Run("start non-existent helper path", func(t *testing.T) {
h := helper.NewDirect(t.Context(), "/proc/nonexistent", argsWt, false, argF, nil, nil) h := helper.NewDirect(t.Context(), container.Nonexistent, argsWt, false, argF, nil, nil)
if err := h.Start(); !errors.Is(err, os.ErrNotExist) { if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
t.Errorf("Start: error = %v, wantErr %v", t.Errorf("Start: error = %v, wantErr %v",

View File

@ -4,20 +4,17 @@ import (
"context" "context"
"io" "io"
"os" "os"
"os/exec"
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
) )
func TestContainer(t *testing.T) { func TestContainer(t *testing.T) {
t.Run("start empty container", func(t *testing.T) { t.Run("start empty container", func(t *testing.T) {
h := helper.New(t.Context(), "/nonexistent", argsWt, false, argF, nil, nil) h := helper.New(t.Context(), container.Nonexistent, argsWt, false, argF, nil, nil)
wantErr := "sandbox: starting an empty container" wantErr := "container: starting an empty container"
if err := h.Start(); err == nil || err.Error() != wantErr { if err := h.Start(); err == nil || err.Error() != wantErr {
t.Errorf("Start: error = %v, wantErr %q", t.Errorf("Start: error = %v, wantErr %q",
err, wantErr) err, wantErr)
@ -36,20 +33,8 @@ func TestContainer(t *testing.T) {
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper { testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
return helper.New(ctx, os.Args[0], argsWt, stat, argF, func(z *container.Container) { return helper.New(ctx, os.Args[0], argsWt, stat, argF, func(z *container.Container) {
setOutput(&z.Stdout, &z.Stderr) setOutput(&z.Stdout, &z.Stderr)
z.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
return exec.CommandContext(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperInit", "--", "init")
}
z.Bind("/", "/", 0).Proc("/proc").Dev("/dev") z.Bind("/", "/", 0).Proc("/proc").Dev("/dev")
}, nil) }, nil)
}) })
}) })
} }
func TestHelperInit(t *testing.T) {
if len(os.Args) != 5 || os.Args[4] != "init" {
return
}
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, func(bool) { internal.InstallOutput(false) })
}

View File

@ -38,7 +38,6 @@ func argF(argsFd, statFd int) []string {
func argFChecked(argsFd, statFd int) (args []string) { func argFChecked(argsFd, statFd int) (args []string) {
args = make([]string, 0, 6) args = make([]string, 0, 6)
args = append(args, "-test.run=TestHelperStub", "--")
if argsFd > -1 { if argsFd > -1 {
args = append(args, "--args", strconv.Itoa(argsFd)) args = append(args, "--args", strconv.Itoa(argsFd))
} }

View File

@ -25,7 +25,7 @@ func InternalHelperStub() {
sp = v sp = v
} }
genericStub(flagRestoreFiles(3, ap, sp)) genericStub(flagRestoreFiles(1, ap, sp))
os.Exit(0) os.Exit(0)
} }

View File

@ -1,9 +1,17 @@
package helper_test package helper_test
import ( import (
"os"
"testing" "testing"
"hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
) )
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() } func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
helper.InternalHelperStub()
os.Exit(m.Run())
}

View File

@ -1,6 +1,8 @@
package hst package hst
import ( import (
"time"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
) )
@ -10,6 +12,10 @@ type (
// container hostname // container hostname
Hostname string `json:"hostname,omitempty"` Hostname string `json:"hostname,omitempty"`
// duration to wait for after interrupting a container's initial process in nanoseconds;
// a negative value causes the container to be terminated immediately on cancellation
WaitDelay time.Duration `json:"wait_delay,omitempty"`
// extra seccomp flags // extra seccomp flags
SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"` SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"`
// extra seccomp presets // extra seccomp presets

View File

@ -62,8 +62,10 @@ func Template() *Config {
Userns: true, Userns: true,
Net: true, Net: true,
Device: true, Device: true,
WaitDelay: -1,
SeccompFlags: seccomp.AllowMultiarch, SeccompFlags: seccomp.AllowMultiarch,
SeccompPresets: seccomp.PresetExt, SeccompPresets: seccomp.PresetExt,
SeccompCompat: true,
Tty: true, Tty: true,
Multiarch: true, Multiarch: true,
MapRealUID: true, MapRealUID: true,

View File

@ -80,8 +80,10 @@ func TestTemplate(t *testing.T) {
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,

View File

@ -144,6 +144,7 @@ var testCasesNixos = []sealTestCase{
Tmpfs("/var/run/nscd", 8192, 0755), Tmpfs("/var/run/nscd", 8192, 0755),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
ForwardCancel: true,
}, },
}, },
} }

View File

@ -71,6 +71,7 @@ var testCasesPd = []sealTestCase{
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
RetainSession: true, RetainSession: true,
ForwardCancel: true,
}, },
}, },
{ {
@ -220,6 +221,7 @@ var testCasesPd = []sealTestCase{
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
RetainSession: true, RetainSession: true,
ForwardCancel: true,
}, },
}, },
} }

View File

@ -32,6 +32,10 @@ func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*contain
SeccompPresets: s.SeccompPresets, SeccompPresets: s.SeccompPresets,
RetainSession: s.Tty, RetainSession: s.Tty,
HostNet: s.Net, HostNet: s.Net,
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
// this behaviour is implemented in the shim
ForwardCancel: s.WaitDelay >= 0,
} }
{ {

View File

@ -123,7 +123,15 @@ func (seal *outcome) Run(rs *RunState) error {
// this prevents blocking forever on an early failure // this prevents blocking forever on an early failure
waitErr, setupErr := make(chan error, 1), make(chan error, 1) waitErr, setupErr := make(chan error, 1), make(chan error, 1)
go func() { waitErr <- cmd.Wait(); cancel() }() go func() { waitErr <- cmd.Wait(); cancel() }()
go func() { setupErr <- e.Encode(&shimParams{os.Getpid(), seal.container, seal.user.data, hlog.Load()}) }() go func() {
setupErr <- e.Encode(&shimParams{
os.Getpid(),
seal.waitDelay,
seal.container,
seal.user.data,
hlog.Load(),
})
}()
select { select {
case err := <-setupErr: case err := <-setupErr:

View File

@ -15,6 +15,7 @@ import (
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
@ -79,6 +80,7 @@ type outcome struct {
sys *system.I sys *system.I
ctx context.Context ctx context.Context
waitDelay time.Duration
container *container.Params container *container.Params
env map[string]string env map[string]string
sync *os.File sync *os.File
@ -281,6 +283,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
var uid, gid int var uid, gid int
var err error var err error
seal.container, seal.env, err = newContainer(config.Container, sys, &uid, &gid) seal.container, seal.env, err = newContainer(config.Container, sys, &uid, &gid)
seal.waitDelay = config.Container.WaitDelay
if err != nil { if err != nil {
return hlog.WrapErrSuffix(err, return hlog.WrapErrSuffix(err,
"cannot initialise container configuration:") "cannot initialise container configuration:")

View File

@ -0,0 +1,65 @@
#include "shim-signal.h"
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static pid_t hakurei_shim_param_ppid = -1;
static int hakurei_shim_fd = -1;
static ssize_t hakurei_shim_write(const void *buf, size_t count) {
int savedErrno = errno;
ssize_t ret = write(hakurei_shim_fd, buf, count);
if (ret == -1 && errno != EAGAIN)
exit(EXIT_FAILURE);
errno = savedErrno;
return ret;
}
/* see shim_linux.go for handling of the value */
static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
if (sig != SIGCONT || si == NULL) {
/* unreachable */
hakurei_shim_write("\2", 1);
return;
}
if (si->si_pid == hakurei_shim_param_ppid) {
/* monitor requests shim exit */
hakurei_shim_write("\0", 1);
return;
}
/* unexpected si_pid */
hakurei_shim_write("\3", 1);
if (getppid() != hakurei_shim_param_ppid)
/* shim orphaned before monitor delivers a signal */
hakurei_shim_write("\1", 1);
}
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd) {
if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1)
*(int *)NULL = 0; /* unreachable */
struct sigaction new_action = {0}, old_action = {0};
if (sigaction(SIGCONT, NULL, &old_action) != 0)
return;
if (old_action.sa_handler != SIG_DFL) {
errno = ENOTRECOVERABLE;
return;
}
new_action.sa_sigaction = hakurei_shim_sigaction;
if (sigemptyset(&new_action.sa_mask) != 0)
return;
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
if (sigaction(SIGCONT, &new_action, NULL) != 0)
return;
errno = 0;
hakurei_shim_param_ppid = ppid;
hakurei_shim_fd = fd;
}

View File

@ -0,0 +1,3 @@
#include <signal.h>
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd);

View File

@ -3,10 +3,13 @@ package app
import ( import (
"context" "context"
"errors" "errors"
"io"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"runtime"
"sync/atomic"
"syscall" "syscall"
"time" "time"
@ -16,55 +19,7 @@ import (
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
) )
/* //#include "shim-signal.h"
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <signal.h>
static pid_t hakurei_shim_param_ppid = -1;
// this cannot unblock hlog since Go code is not async-signal-safe
static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
if (sig != SIGCONT || si == NULL) {
// unreachable
fprintf(stderr, "sigaction: sa_sigaction got invalid siginfo\n");
return;
}
// monitor requests shim exit
if (si->si_pid == hakurei_shim_param_ppid)
exit(254);
fprintf(stderr, "sigaction: got SIGCONT from process %d\n", si->si_pid);
// shim orphaned before monitor delivers a signal
if (getppid() != hakurei_shim_param_ppid)
exit(3);
}
void hakurei_shim_setup_cont_signal(pid_t ppid) {
struct sigaction new_action = {0}, old_action = {0};
if (sigaction(SIGCONT, NULL, &old_action) != 0)
return;
if (old_action.sa_handler != SIG_DFL) {
errno = ENOTRECOVERABLE;
return;
}
new_action.sa_sigaction = hakurei_shim_sigaction;
if (sigemptyset(&new_action.sa_mask) != 0)
return;
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
if (sigaction(SIGCONT, &new_action, NULL) != 0)
return;
errno = 0;
hakurei_shim_param_ppid = ppid;
}
*/
import "C" import "C"
const shimEnv = "HAKUREI_SHIM" const shimEnv = "HAKUREI_SHIM"
@ -73,6 +28,10 @@ type shimParams struct {
// monitor pid, checked against ppid in signal handler // monitor pid, checked against ppid in signal handler
Monitor int Monitor int
// duration to wait for after interrupting a container's initial process before the container is killed;
// zero value defaults to [DefaultShimWaitDelay], values exceeding [MaxShimWaitDelay] becomes [MaxShimWaitDelay]
WaitDelay time.Duration
// finalised container params // finalised container params
Container *container.Params Container *container.Params
// path to outer home directory // path to outer home directory
@ -82,6 +41,16 @@ type shimParams struct {
Verbose bool Verbose bool
} }
const (
// ShimExitRequest is returned when the monitor process requests shim exit.
ShimExitRequest = 254
// ShimExitOrphan is returned when the shim is orphaned before monitor delivers a signal.
ShimExitOrphan = 3
DefaultShimWaitDelay = 5 * time.Second
MaxShimWaitDelay = 30 * time.Second
)
// ShimMain is the main function of the shim process and runs as the unconstrained target user. // ShimMain is the main function of the shim process and runs as the unconstrained target user.
func ShimMain() { func ShimMain() {
hlog.Prepare("shim") hlog.Prepare("shim")
@ -106,18 +75,63 @@ func ShimMain() {
} else { } else {
internal.InstallOutput(params.Verbose) internal.InstallOutput(params.Verbose)
closeSetup = f closeSetup = f
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(params.Monitor)); err != nil {
log.Fatalf("cannot install SIGCONT handler: %v", err)
}
// pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); errno != 0 {
log.Fatalf("cannot set parent-death signal: %v", errno)
}
} }
var signalPipe io.ReadCloser
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
if r, w, err := os.Pipe(); err != nil {
log.Fatalf("cannot pipe: %v", err)
} else if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(params.Monitor), C.int(w.Fd())); err != nil {
log.Fatalf("cannot install SIGCONT handler: %v", err)
} else {
defer runtime.KeepAlive(w)
signalPipe = r
}
// pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); errno != 0 {
log.Fatalf("cannot set parent-death signal: %v", errno)
}
// signal handler outcome
var cancelContainer atomic.Pointer[context.CancelFunc]
go func() {
buf := make([]byte, 1)
for {
if _, err := signalPipe.Read(buf); err != nil {
log.Fatalf("cannot read from signal pipe: %v", err)
}
switch buf[0] {
case 0: // got SIGCONT from monitor: shim exit requested
if fp := cancelContainer.Load(); params.Container.ForwardCancel && fp != nil && *fp != nil {
(*fp)()
// shim now bound by ShimWaitDelay, implemented below
continue
}
// setup has not completed, terminate immediately
hlog.Resume()
os.Exit(ShimExitRequest)
return
case 1: // got SIGCONT after adoption: monitor died before delivering signal
hlog.BeforeExit()
os.Exit(ShimExitOrphan)
return
case 2: // unreachable
log.Println("sa_sigaction got invalid siginfo")
case 3: // got SIGCONT from unexpected process: hopefully the terminal driver
log.Println("got SIGCONT from unexpected process")
default: // unreachable
log.Fatalf("got invalid message %d from signal handler", buf[0])
}
}
}()
if params.Container == nil || params.Container.Ops == nil { if params.Container == nil || params.Container.Ops == nil {
log.Fatal("invalid container params") log.Fatal("invalid container params")
} }
@ -148,12 +162,18 @@ func ShimMain() {
name = params.Container.Args[0] name = params.Container.Args[0]
} }
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() // unreachable cancelContainer.Store(&stop)
z := container.New(ctx, name) z := container.New(ctx, name)
z.Params = *params.Container z.Params = *params.Container
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
z.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
z.WaitDelay = 2 * time.Second z.WaitDelay = params.WaitDelay
if z.WaitDelay == 0 {
z.WaitDelay = DefaultShimWaitDelay
}
if z.WaitDelay > MaxShimWaitDelay {
z.WaitDelay = MaxShimWaitDelay
}
if err := z.Start(); err != nil { if err := z.Start(); err != nil {
hlog.PrintBaseError(err, "cannot start container:") hlog.PrintBaseError(err, "cannot start container:")

View File

@ -5,7 +5,6 @@ import (
"context" "context"
"io" "io"
"os" "os"
"os/exec"
"time" "time"
"hakurei.app/container" "hakurei.app/container"
@ -19,16 +18,10 @@ var (
msgStaticGlibc = []byte("not a dynamic executable") msgStaticGlibc = []byte("not a dynamic executable")
) )
func Exec(ctx context.Context, p string) ([]*Entry, error) { return ExecFilter(ctx, nil, nil, p) } func Exec(ctx context.Context, p string) ([]*Entry, error) {
func ExecFilter(ctx context.Context,
commandContext func(context.Context) *exec.Cmd,
f func([]byte) []byte,
p string) ([]*Entry, error) {
c, cancel := context.WithTimeout(ctx, lddTimeout) c, cancel := context.WithTimeout(ctx, lddTimeout)
defer cancel() defer cancel()
z := container.New(c, "ldd", p) z := container.New(c, "ldd", p)
z.CommandContext = commandContext
z.Hostname = "hakurei-ldd" z.Hostname = "hakurei-ldd"
z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompFlags |= seccomp.AllowMultiarch
z.SeccompPresets |= seccomp.PresetStrict z.SeccompPresets |= seccomp.PresetStrict
@ -54,8 +47,5 @@ func ExecFilter(ctx context.Context,
} }
v := stdout.Bytes() v := stdout.Bytes()
if f != nil {
v = f(v)
}
return Parse(v) return Parse(v)
} }

View File

@ -82,7 +82,8 @@ in
own = [ own = [
"${id}.*" "${id}.*"
"org.mpris.MediaPlayer2.${id}.*" "org.mpris.MediaPlayer2.${id}.*"
] ++ ext.own; ]
++ ext.own;
inherit (ext) call broadcast; inherit (ext) call broadcast;
}; };
@ -127,6 +128,7 @@ in
container = { container = {
inherit (app) inherit (app)
wait_delay
devel devel
userns userns
net net
@ -175,27 +177,26 @@ in
auto_etc = true; auto_etc = true;
cover = [ "/var/run/nscd" ]; cover = [ "/var/run/nscd" ];
symlink = symlink = [
[
"*/run/current-system"
"/run/current-system"
]
]
++ optionals (isGraphical && config.hardware.graphics.enable) (
[ [
[ [
"*/run/current-system" config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument
"/run/current-system" "/run/opengl-driver"
] ]
] ]
++ optionals (isGraphical && config.hardware.graphics.enable) ( ++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [
[ [
[ config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument
config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument /run/opengl-driver-32
"/run/opengl-driver"
]
] ]
++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [ ]
[ );
config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument
/run/opengl-driver-32
]
]
);
}; };
}; };

View File

@ -76,6 +76,7 @@ in
type = type =
let let
inherit (types) inherit (types)
int
ints ints
str str
bool bool
@ -195,6 +196,16 @@ in
''; '';
}; };
wait_delay = mkOption {
type = nullOr int;
default = null;
description = ''
Duration to wait for after interrupting a container's initial process in nanoseconds.
A negative value causes the container to be terminated immediately on cancellation.
Setting this to null defaults to five seconds.
'';
};
devel = mkEnableOption "debugging-related kernel interfaces"; devel = mkEnableOption "debugging-related kernel interfaces";
userns = mkEnableOption "user namespace creation"; userns = mkEnableOption "user namespace creation";
tty = mkEnableOption "access to the controlling terminal"; tty = mkEnableOption "access to the controlling terminal";

View File

@ -31,7 +31,7 @@
buildGoModule rec { buildGoModule rec {
pname = "hakurei"; pname = "hakurei";
version = "0.1.1"; version = "0.1.2";
srcFiltered = builtins.path { srcFiltered = builtins.path {
name = "${pname}-src"; name = "${pname}-src";
@ -83,18 +83,17 @@ buildGoModule rec {
# nix build environment does not allow acls # nix build environment does not allow acls
env.GO_TEST_SKIP_ACL = 1; env.GO_TEST_SKIP_ACL = 1;
buildInputs = buildInputs = [
[ libffi
libffi libseccomp
libseccomp acl
acl wayland
wayland ]
] ++ (with xorg; [
++ (with xorg; [ libxcb
libxcb libXau
libXau libXdmcp
libXdmcp ]);
]);
nativeBuildInputs = [ nativeBuildInputs = [
pkg-config pkg-config
@ -130,17 +129,16 @@ buildGoModule rec {
} }
''; '';
passthru.targetPkgs = passthru.targetPkgs = [
[ go
go gcc
gcc xorg.xorgproto
xorg.xorgproto util-linux
util-linux
# for go generate # for go generate
wayland-protocols wayland-protocols
wayland-scanner wayland-scanner
] ]
++ buildInputs ++ buildInputs
++ nativeBuildInputs; ++ nativeBuildInputs;
} }

View File

@ -3,6 +3,7 @@ package system
import ( import (
"testing" "testing"
"hakurei.app/container"
"hakurei.app/system/acl" "hakurei.app/system/acl"
) )
@ -52,19 +53,19 @@ func TestACLString(t *testing.T) {
et Enablement et Enablement
perms []acl.Perm perms []acl.Perm
}{ }{
{`--- type: process path: "/nonexistent"`, Process, []acl.Perm{}}, {`--- type: process path: "/proc/nonexistent"`, Process, []acl.Perm{}},
{`r-- type: user path: "/nonexistent"`, User, []acl.Perm{acl.Read}}, {`r-- type: user path: "/proc/nonexistent"`, User, []acl.Perm{acl.Read}},
{`-w- type: wayland path: "/nonexistent"`, EWayland, []acl.Perm{acl.Write}}, {`-w- type: wayland path: "/proc/nonexistent"`, EWayland, []acl.Perm{acl.Write}},
{`--x type: x11 path: "/nonexistent"`, EX11, []acl.Perm{acl.Execute}}, {`--x type: x11 path: "/proc/nonexistent"`, EX11, []acl.Perm{acl.Execute}},
{`rw- type: dbus path: "/nonexistent"`, EDBus, []acl.Perm{acl.Read, acl.Write}}, {`rw- type: dbus path: "/proc/nonexistent"`, EDBus, []acl.Perm{acl.Read, acl.Write}},
{`r-x type: pulseaudio path: "/nonexistent"`, EPulse, []acl.Perm{acl.Read, acl.Execute}}, {`r-x type: pulseaudio path: "/proc/nonexistent"`, EPulse, []acl.Perm{acl.Read, acl.Execute}},
{`rwx type: user path: "/nonexistent"`, User, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, {`rwx type: user path: "/proc/nonexistent"`, User, []acl.Perm{acl.Read, acl.Write, acl.Execute}},
{`rwx type: process path: "/nonexistent"`, Process, []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}}, {`rwx type: process path: "/proc/nonexistent"`, Process, []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
a := &ACL{et: tc.et, perms: tc.perms, path: "/nonexistent"} a := &ACL{et: tc.et, perms: tc.perms, path: container.Nonexistent}
if got := a.String(); got != tc.want { if got := a.String(); got != tc.want {
t.Errorf("String() = %v, want %v", t.Errorf("String() = %v, want %v",
got, tc.want) got, tc.want)

View File

@ -1,22 +1,17 @@
package dbus_test package dbus_test
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"strings" "strings"
"syscall" "syscall"
"testing" "testing"
"time" "time"
"hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -64,20 +59,23 @@ func TestFinalise(t *testing.T) {
} }
func TestProxyStartWaitCloseString(t *testing.T) { func TestProxyStartWaitCloseString(t *testing.T) {
oldWaitDelay := helper.WaitDelay t.Run("sandbox", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, true) })
helper.WaitDelay = 16 * time.Second
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
t.Run("sandbox", func(t *testing.T) {
proxyName := dbus.ProxyName
dbus.ProxyName = os.Args[0]
t.Cleanup(func() { dbus.ProxyName = proxyName })
testProxyFinaliseStartWaitCloseString(t, true)
})
t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) }) t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) })
} }
func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
{
oldWaitDelay := helper.WaitDelay
helper.WaitDelay = 16 * time.Second
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
}
{
proxyName := dbus.ProxyName
dbus.ProxyName = os.Args[0]
t.Cleanup(func() { dbus.ProxyName = proxyName })
}
var p *dbus.Proxy var p *dbus.Proxy
t.Run("string for nil proxy", func(t *testing.T) { t.Run("string for nil proxy", func(t *testing.T) {
@ -122,35 +120,12 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel() defer cancel()
if !useSandbox {
p = dbus.NewDirect(ctx, final, nil)
} else {
p = dbus.New(ctx, final, nil)
}
p.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
return exec.CommandContext(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperInit", "--", "init")
}
p.CmdF = func(v any) {
if useSandbox {
z := v.(*container.Container)
if z.Args[0] != dbus.ProxyName {
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
}
z.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, z.Args[1:]...)
} else {
cmd := v.(*exec.Cmd)
if cmd.Args[0] != dbus.ProxyName {
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
}
cmd.Err = nil
cmd.Path = os.Args[0]
cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args[1:]...)
}
}
p.FilterF = func(v []byte) []byte { return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1] }
output := new(strings.Builder) output := new(strings.Builder)
if !useSandbox {
p = dbus.NewDirect(ctx, final, output)
} else {
p = dbus.New(ctx, final, output)
}
t.Run("invalid wait", func(t *testing.T) { t.Run("invalid wait", func(t *testing.T) {
wantErr := "dbus: not started" wantErr := "dbus: not started"
@ -176,9 +151,9 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
} }
t.Run("string", func(t *testing.T) { t.Run("string", func(t *testing.T) {
wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0]) wantSubstr := fmt.Sprintf("%s --args=3 --fd=4", os.Args[0])
if useSandbox { if useSandbox {
wantSubstr = fmt.Sprintf(`argv: ["%s" "-test.run=TestHelperStub" "--" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf`, os.Args[0]) wantSubstr = fmt.Sprintf(`argv: ["%s" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf`, os.Args[0])
} }
if got := p.String(); !strings.Contains(got, wantSubstr) { if got := p.String(); !strings.Contains(got, wantSubstr) {
t.Errorf("String: %q, want %q", t.Errorf("String: %q, want %q",
@ -203,11 +178,3 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
}) })
} }
} }
func TestHelperInit(t *testing.T) {
if len(os.Args) != 5 || os.Args[4] != "init" {
return
}
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, internal.InstallOutput)
}

View File

@ -36,9 +36,6 @@ func (p *Proxy) Start() error {
if !p.useSandbox { if !p.useSandbox {
p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) { p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) {
if p.CmdF != nil {
p.CmdF(cmd)
}
if p.output != nil { if p.output != nil {
cmd.Stdout, cmd.Stderr = p.output, p.output cmd.Stdout, cmd.Stderr = p.output, p.output
} }
@ -56,7 +53,7 @@ func (p *Proxy) Start() error {
} }
var libPaths []string var libPaths []string
if entries, err := ldd.ExecFilter(ctx, p.CommandContext, p.FilterF, toolPath); err != nil { if entries, err := ldd.Exec(ctx, toolPath); err != nil {
return err return err
} else { } else {
libPaths = ldd.Path(entries) libPaths = ldd.Path(entries)
@ -69,15 +66,10 @@ func (p *Proxy) Start() error {
z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompFlags |= seccomp.AllowMultiarch
z.SeccompPresets |= seccomp.PresetStrict z.SeccompPresets |= seccomp.PresetStrict
z.Hostname = "hakurei-dbus" z.Hostname = "hakurei-dbus"
z.CommandContext = p.CommandContext
if p.output != nil { if p.output != nil {
z.Stdout, z.Stderr = p.output, p.output z.Stdout, z.Stderr = p.output, p.output
} }
if p.CmdF != nil {
p.CmdF(z)
}
// these lib paths are unpredictable, so mount them first so they cannot cover anything // these lib paths are unpredictable, so mount them first so they cannot cover anything
for _, name := range libPaths { for _, name := range libPaths {
z.Bind(name, name, 0) z.Bind(name, name, 0)

17
system/dbus/proc_test.go Normal file
View File

@ -0,0 +1,17 @@
package dbus_test
import (
"os"
"testing"
"hakurei.app/container"
"hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
helper.InternalHelperStub()
os.Exit(m.Run())
}

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"os/exec"
"sync" "sync"
"syscall" "syscall"
@ -37,10 +36,6 @@ type Proxy struct {
useSandbox bool useSandbox bool
name string name string
CmdF func(any)
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
FilterF func([]byte) []byte
mu, pmu sync.RWMutex mu, pmu sync.RWMutex
} }

View File

@ -1,9 +0,0 @@
package dbus_test
import (
"testing"
"hakurei.app/helper"
)
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }

View File

@ -3,6 +3,8 @@ package system
import ( import (
"os" "os"
"testing" "testing"
"hakurei.app/container"
) )
func TestEnsure(t *testing.T) { func TestEnsure(t *testing.T) {
@ -60,11 +62,11 @@ func TestMkdirString(t *testing.T) {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
m := &Mkdir{ m := &Mkdir{
et: tc.et, et: tc.et,
path: "/nonexistent", path: container.Nonexistent,
perm: 0701, perm: 0701,
ephemeral: tc.ephemeral, ephemeral: tc.ephemeral,
} }
want := "mode: " + os.FileMode(0701).String() + " type: " + tc.want + " path: \"/nonexistent\"" want := "mode: " + os.FileMode(0701).String() + " type: " + tc.want + ` path: "/proc/nonexistent"`
if got := m.String(); got != want { if got := m.String(); got != want {
t.Errorf("String() = %v, want %v", got, want) t.Errorf("String() = %v, want %v", got, want)
} }

View File

@ -127,6 +127,21 @@
}; };
}; };
"cat.gensokyo.extern.foot.noEnablements.immediate" = {
name = "ne-foot-immediate";
identity = 1;
shareUid = true;
verbose = true;
wait_delay = -1;
share = pkgs.foot;
packages = [ ];
command = "foot";
capability = {
dbus = false;
pulse = false;
};
};
"cat.gensokyo.extern.foot.pulseaudio" = { "cat.gensokyo.extern.foot.pulseaudio" = {
name = "pa-foot"; name = "pa-foot";
identity = 2; identity = 2;

View File

@ -173,13 +173,6 @@ in
} null; } null;
} null; } null;
".local" = fs "800001ed" { ".local" = fs "800001ed" {
share = fs "800001ed" {
dbus-1 = fs "800001ed" {
services = fs "800001ed" {
"ca.desrt.dconf.service" = fs "80001ff" null null;
} null;
} null;
} null;
state = fs "800001ed" { state = fs "800001ed" {
".keep" = fs "80001ff" null ""; ".keep" = fs "80001ff" null "";
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;

View File

@ -199,13 +199,6 @@ in
} null; } null;
} null; } null;
".local" = fs "800001ed" { ".local" = fs "800001ed" {
share = fs "800001ed" {
dbus-1 = fs "800001ed" {
services = fs "800001ed" {
"ca.desrt.dconf.service" = fs "80001ff" null null;
} null;
} null;
} null;
state = fs "800001ed" { state = fs "800001ed" {
".keep" = fs "80001ff" null ""; ".keep" = fs "80001ff" null "";
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;

View File

@ -200,13 +200,6 @@ in
} null; } null;
} null; } null;
".local" = fs "800001ed" { ".local" = fs "800001ed" {
share = fs "800001ed" {
dbus-1 = fs "800001ed" {
services = fs "800001ed" {
"ca.desrt.dconf.service" = fs "80001ff" null null;
} null;
} null;
} null;
state = fs "800001ed" { state = fs "800001ed" {
".keep" = fs "80001ff" null ""; ".keep" = fs "80001ff" null "";
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;

View File

@ -199,13 +199,6 @@ in
} null; } null;
} null; } null;
".local" = fs "800001ed" { ".local" = fs "800001ed" {
share = fs "800001ed" {
dbus-1 = fs "800001ed" {
services = fs "800001ed" {
"ca.desrt.dconf.service" = fs "80001ff" null null;
} null;
} null;
} null;
state = fs "800001ed" { state = fs "800001ed" {
".keep" = fs "80001ff" null ""; ".keep" = fs "80001ff" null "";
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;

View File

@ -200,13 +200,6 @@ in
} null; } null;
} null; } null;
".local" = fs "800001ed" { ".local" = fs "800001ed" {
share = fs "800001ed" {
dbus-1 = fs "800001ed" {
services = fs "800001ed" {
"ca.desrt.dconf.service" = fs "80001ff" null null;
} null;
} null;
} null;
state = fs "800001ed" { state = fs "800001ed" {
".keep" = fs "80001ff" null ""; ".keep" = fs "80001ff" null "";
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;

View File

@ -178,9 +178,28 @@ machine.succeed("pkill -INT -f 'hakurei -v app '")
machine.wait_until_fails("pgrep foot", timeout=5) machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/monitor-exit-code") machine.wait_for_file("/tmp/monitor-exit-code")
interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code")) interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code"))
if interrupt_exit_code != 230:
raise Exception(f"unexpected exit code {interrupt_exit_code}")
# Check interrupt shim behaviour immediate termination:
swaymsg("exec sh -c 'ne-foot-immediate; echo -n $? > /tmp/monitor-exit-code'")
wait_for_window(f"u0_a{aid(0)}@machine")
machine.succeed("pkill -INT -f 'hakurei -v app '")
machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/monitor-exit-code")
interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code"))
if interrupt_exit_code != 254: if interrupt_exit_code != 254:
raise Exception(f"unexpected exit code {interrupt_exit_code}") raise Exception(f"unexpected exit code {interrupt_exit_code}")
# Check shim SIGCONT from unexpected process behaviour:
swaymsg("exec sh -c 'ne-foot &> /tmp/shim-cont-unexpected-pid'")
wait_for_window(f"u0_a{aid(0)}@machine")
machine.succeed("pkill -CONT -f 'hakurei shim'")
machine.succeed("pkill -INT -f 'hakurei -v app '")
machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/shim-cont-unexpected-pid")
print(machine.succeed('grep "shim: got SIGCONT from unexpected process$" /tmp/shim-cont-unexpected-pid'))
# Start app (foot) with Wayland enablement: # Start app (foot) with Wayland enablement:
swaymsg("exec ne-foot") swaymsg("exec ne-foot")
wait_for_window(f"u0_a{aid(0)}@machine") wait_for_window(f"u0_a{aid(0)}@machine")