Compare commits

..

No commits in common. "cb513bb1cd7b098bbe8a60cd105cc764648db410" and "6ba19a7ba5c36a4276dceeab9a5d158c5d9738a0" have entirely different histories.

45 changed files with 517 additions and 801 deletions

View File

@ -256,10 +256,8 @@ 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,
@ -384,10 +382,8 @@ 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,
@ -566,10 +562,8 @@ 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,7 +215,8 @@ stdenv.mkDerivation {
# create binary cache # create binary cache
closureInfo="${ closureInfo="${
closureInfo { closureInfo {
rootPaths = [ rootPaths =
[
homeManagerConfiguration.activationPackage homeManagerConfiguration.activationPackage
launcher launcher
] ]

View File

@ -17,16 +17,6 @@ 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.
@ -39,6 +29,9 @@ 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
@ -66,10 +59,6 @@ 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
@ -79,7 +68,6 @@ 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.
@ -88,7 +76,6 @@ 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
@ -101,13 +88,12 @@ 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("container: already started") return errors.New("sandbox: already started")
} }
if p.Ops == nil || len(*p.Ops) == 0 { if p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("container: starting an empty container") return errors.New("sandbox: starting an empty container")
} }
ctx, cancel := context.WithCancel(p.ctx) ctx, cancel := context.WithCancel(p.ctx)
@ -130,22 +116,19 @@ func (p *Container) Start() error {
p.SeccompPresets |= seccomp.PresetDenyTTY p.SeccompPresets |= seccomp.PresetDenyTTY
} }
if p.AdoptWaitDelay == 0 { if p.CommandContext != nil {
p.AdoptWaitDelay = 5 * time.Second p.cmd = p.CommandContext(ctx)
} } else {
// to allow disabling this behaviour p.cmd = exec.CommandContext(ctx, MustExecutable())
if p.AdoptWaitDelay < 0 { p.cmd.Args = []string{"init"}
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(CancelSignal) } p.cmd.Cancel = func() error { return p.cmd.Process.Signal(SIGTERM) }
} }
p.cmd.Dir = "/" p.cmd.Dir = "/"
p.cmd.SysProcAttr = &SysProcAttr{ p.cmd.SysProcAttr = &SysProcAttr{
@ -179,8 +162,6 @@ 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")
@ -234,7 +215,6 @@ 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 {
@ -242,14 +222,6 @@ 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,85 +4,28 @@ 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()
@ -92,86 +35,124 @@ func TestContainer(t *testing.T) {
t.Cleanup(func() { container.SetOutput(oldOutput) }) t.Cleanup(func() { container.SetOutput(oldOutput) })
} }
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) { testCases := []struct {
wantErr := context.Canceled name string
wantExitCode := 0 filter bool
if err := c.Wait(); !errors.Is(err, wantErr) { session bool
hlog.PrintBaseError(err, "wait:") net bool
t.Errorf("Wait: error = %v, want %v", err, wantErr) ops *container.Ops
mnt []*vfs.MountInfoEntry
host string
rules []seccomp.NativeRule
flags seccomp.ExportFlag
presets seccomp.FilterPreset
}{
{"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},
} }
if ps := c.ProcessState(); ps == nil {
t.Errorf("ProcessState unexpectedly returned nil")
} else if code := ps.ExitCode(); code != wantExitCode {
t.Errorf("ExitCode: %d, want %d", code, wantExitCode)
}
}))
t.Run("forward", testContainerCancel(func(c *container.Container) { for _, tc := range testCases {
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(), helperDefaultTimeout) ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel() defer cancel()
var libPaths []string c := container.New(ctx, "/usr/bin/sandbox.test", "-test.v",
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i)) "-test.run=TestHelperCheckContainer", "--", "check", tc.host)
c.Uid = tc.uid c.Uid = 1000
c.Gid = tc.gid c.Gid = 100
c.Hostname = hostnameFromTestCase(tc.name) c.Hostname = tc.host
c.CommandContext = commandContext
c.Stdout, c.Stderr = os.Stdout, os.Stderr c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.WaitDelay = helperDefaultTimeout c.Ops = tc.ops
*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).
Place("/etc/hostname", []byte(c.Hostname)) Bind(os.Args[0], os.Args[0], 0).
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, mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
ent("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
// Bind(os.Args[0], helperInnerPath, 0)
ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
)
for _, name := range libPaths {
// Bind(name, name, 0)
mnt = append(mnt, ent(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
}
mnt = append(mnt, tc.mnt...) mnt = append(mnt, tc.mnt...)
mnt = append(mnt, mnt = append(mnt,
// Tmpfs("/tmp", 0, 0755) e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
// Place("/etc/hostname", []byte(hostname)) e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
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),
) )
for _, name := range libPaths {
mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
}
mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"))
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.Place(pathWantMnt, want.Bytes()) c.Stdin = want
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
hlog.PrintBaseError(err, "start:") hlog.PrintBaseError(err, "start:")
@ -188,7 +169,7 @@ func TestContainer(t *testing.T) {
} }
} }
func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry { func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
return &vfs.MountInfoEntry{ return &vfs.MountInfoEntry{
ID: ignoreV, ID: ignoreV,
Parent: ignoreV, Parent: ignoreV,
@ -203,50 +184,6 @@ func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInf
} }
} }
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
@ -260,70 +197,49 @@ func TestContainerString(t *testing.T) {
} }
} }
const ( func TestHelperInit(t *testing.T) {
blockExitCodeInterrupt = 2 if len(os.Args) != 5 || os.Args[4] != "init" {
) return
}
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, internal.InstallOutput)
}
func init() { func TestHelperCheckContainer(t *testing.T) {
helperCommands = append(helperCommands, func(c command.Command) { if len(os.Args) != 6 || os.Args[4] != "check" {
c.Command("block", command.UsageInternal, func(args []string) error { return
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
} }
{
sig := make(chan os.Signal, 1) t.Run("user", func(t *testing.T) {
signal.Notify(sig, os.Interrupt) if uid := syscall.Getuid(); uid != 1000 {
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }() t.Errorf("Getuid: %d, want 1000", uid)
}
if gid := syscall.Getgid(); gid != 100 {
t.Errorf("Getgid: %d, want 100", gid)
} }
select {}
}) })
t.Run("hostname", func(t *testing.T) {
c.Command("container", command.UsageInternal, func(args []string) error { if name, err := os.Hostname(); err != nil {
if len(args) != 1 { t.Fatalf("cannot get hostname: %v", err)
return syscall.EINVAL } else if name != os.Args[5] {
} t.Errorf("Hostname: %q, want %q", name, os.Args[5])
tc := containerTestCases[0]
if i, err := strconv.Atoi(args[0]); err != nil {
return fmt.Errorf("cannot parse test case index: %v", err)
} else {
tc = containerTestCases[i]
}
if uid := syscall.Getuid(); uid != tc.uid {
return fmt.Errorf("uid: %d, want %d", uid, tc.uid)
}
if gid := syscall.Getgid(); gid != tc.gid {
return fmt.Errorf("gid: %d, want %d", gid, tc.gid)
}
wantHost := hostnameFromTestCase(tc.name)
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 { if p, err := os.ReadFile("/etc/hostname"); err != nil {
return fmt.Errorf("cannot read /etc/hostname: %v", err) t.Fatalf("%v", err)
} else if string(p) != wantHost { } else if string(p) != os.Args[5] {
return fmt.Errorf("/etc/hostname: %q, want %q", string(p), wantHost) t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
} }
})
{ t.Run("mount", func(t *testing.T) {
var fail bool
var mnt []*vfs.MountInfoEntry var mnt []*vfs.MountInfoEntry
if f, err := os.Open(pathWantMnt); err != nil { if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
return fmt.Errorf("cannot open expected mount points: %v", err) t.Fatalf("cannot receive 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 var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil { if f, err := os.Open("/proc/self/mountinfo"); err != nil {
return fmt.Errorf("cannot open mountinfo: %v", err) t.Fatalf("cannot open mountinfo: %v", err)
} else { } else {
d = vfs.NewMountInfoDecoder(f) d = vfs.NewMountInfoDecoder(f)
} }
@ -331,7 +247,8 @@ func init() {
i := 0 i := 0
for cur := range d.Entries() { for cur := range d.Entries() {
if i == len(mnt) { if i == len(mnt) {
return fmt.Errorf("got more than %d entries", len(mnt)) t.Errorf("got more than %d entries", len(mnt))
break
} }
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags // ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
@ -341,28 +258,24 @@ func init() {
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime") mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") { if !cur.EqualWithIgnore(mnt[i], "\x00") {
fail = true t.Errorf("[FAIL] %s", cur)
log.Printf("[FAIL] %s", cur)
} else { } else {
log.Printf("[ OK ] %s", cur) t.Logf("[ OK ] %s", cur)
} }
i++ i++
} }
if err := d.Err(); err != nil { if err := d.Err(); err != nil {
return fmt.Errorf("cannot parse mountinfo: %v", err) t.Errorf("cannot parse mountinfo: %v", err)
} }
if i != len(mnt) { if i != len(mnt) {
return fmt.Errorf("got %d entries, want %d", i, len(mnt)) t.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,6 +17,9 @@ 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:
@ -267,14 +270,13 @@ 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.Printf("cannot close setup pipe: %v", err) log.Println("cannot close setup pipe:", err)
// not fatal // not fatal
} }
@ -308,7 +310,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
} }
if !errors.Is(err, ECHILD) { if !errors.Is(err, ECHILD) {
log.Printf("unexpected wait4 response: %v", err) log.Println("unexpected wait4 response:", err)
} }
close(done) close(done)
@ -316,7 +318,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, os.Interrupt, CancelSignal) signal.Notify(sig, SIGINT, SIGTERM)
// 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{})
@ -326,16 +328,9 @@ 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("%s after process start", s.String()) msg.Verbosef("terminating on %s after process start", s.String())
} else { } else {
msg.Verbosef("got %s", s.String()) msg.Verbosef("terminating on %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:
@ -355,7 +350,10 @@ 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() { time.Sleep(params.AdoptWaitDelay); close(timeout) }() go func() {
time.Sleep(residualProcessTimeout)
close(timeout)
}()
} }
case <-done: case <-done:
msg.BeforeExit() msg.BeforeExit()
@ -368,11 +366,9 @@ 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]) == initName { if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
msg = v msg = v
Init(prepare, setVerbose) Init(prepare, setVerbose)
msg.BeforeExit() msg.BeforeExit()

View File

@ -1,69 +0,0 @@
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,9 +15,6 @@ import (
type ( type (
Ops []Op Ops []Op
// Op is a generic setup step ran inside the container init.
// Implementations of this interface are sent as a stream of gobs.
Op interface { Op interface {
// early is called in host root. // early is called in host root.
early(params *Params) error early(params *Params) error
@ -30,17 +27,11 @@ 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)) }
// Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target]. // BindMountOp bind mounts host path Source on container path 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
@ -48,11 +39,8 @@ 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
) )
@ -120,15 +108,14 @@ 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 {
func init() { gob.Register(new(MountProcOp)) } *f = append(*f, &BindMountOp{source, "", target, flags})
// 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 return f
} }
func init() { gob.Register(new(MountProcOp)) }
// MountProcOp mounts a private instance of proc.
type MountProcOp string type MountProcOp string
func (p MountProcOp) early(*Params) error { return nil } func (p MountProcOp) early(*Params) error { return nil }
@ -150,15 +137,14 @@ 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 {
func init() { gob.Register(new(MountDevOp)) } *f = append(*f, MountProcOp(dest))
// 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 return f
} }
func init() { gob.Register(new(MountDevOp)) }
// MountDevOp mounts part of host dev.
type MountDevOp string type MountDevOp string
func (d MountDevOp) early(*Params) error { return nil } func (d MountDevOp) early(*Params) error { return nil }
@ -245,15 +231,14 @@ 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 {
func init() { gob.Register(new(MountMqueueOp)) } *f = append(*f, MountDevOp(dest))
// 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 return f
} }
func init() { gob.Register(new(MountMqueueOp)) }
// MountMqueueOp mounts a private mqueue instance on container Path.
type MountMqueueOp string type MountMqueueOp string
func (m MountMqueueOp) early(*Params) error { return nil } func (m MountMqueueOp) early(*Params) error { return nil }
@ -275,15 +260,14 @@ 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 {
func init() { gob.Register(new(MountTmpfsOp)) } *f = append(*f, MountMqueueOp(dest))
// 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 return f
} }
func init() { gob.Register(new(MountTmpfsOp)) }
// MountTmpfsOp mounts tmpfs on container Path.
type MountTmpfsOp struct { type MountTmpfsOp struct {
Path string Path string
Size int Size int
@ -304,15 +288,14 @@ 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 {
func init() { gob.Register(new(SymlinkOp)) } *f = append(*f, &MountTmpfsOp{dest, size, perm})
// 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 return f
} }
func init() { gob.Register(new(SymlinkOp)) }
// SymlinkOp creates a symlink in the container filesystem.
type SymlinkOp [2]string type SymlinkOp [2]string
func (l *SymlinkOp) early(*Params) error { func (l *SymlinkOp) early(*Params) error {
@ -348,15 +331,14 @@ 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 {
func init() { gob.Register(new(MkdirOp)) } *f = append(*f, &SymlinkOp{target, linkName})
// 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 return f
} }
func init() { gob.Register(new(MkdirOp)) }
// MkdirOp creates a directory in the container filesystem.
type MkdirOp struct { type MkdirOp struct {
Path string Path string
Perm os.FileMode Perm os.FileMode
@ -377,21 +359,14 @@ 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 {
func init() { gob.Register(new(TmpfileOp)) } *f = append(*f, &MkdirOp{dest, perm})
// 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 return f
} }
func init() { gob.Register(new(TmpfileOp)) }
// TmpfileOp places a file in container Path containing Data.
type TmpfileOp struct { type TmpfileOp struct {
Path string Path string
Data []byte Data []byte
@ -440,19 +415,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
func init() { gob.Register(new(AutoEtcOp)) } *f = append(*f, t)
// 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.
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 return f
} }
func init() { gob.Register(new(AutoEtcOp)) }
// AutoEtcOp 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.
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 }
@ -498,3 +473,10 @@ 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 b.Loop() { for i := 0; i < b.N; i++ {
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": 1753479839, "lastModified": 1748665073,
"narHash": "sha256-E/rPVh7vyPMJUFl2NAew+zibNGfVbANr8BP8nLRbLkQ=", "narHash": "sha256-RMhjnPKWtCoIIHiuR9QKD7xfsKb3agxzMfJY8V9MOew=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "0b9bf983db4d064764084cd6748efb1ab8297d1e", "rev": "282e1e029cb6ab4811114fc85110613d72771dea",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -23,11 +23,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1753345091, "lastModified": 1749024892,
"narHash": "sha256-CdX2Rtvp5I8HGu9swBmYuq+ILwRxpXdJwlpg8jvN4tU=", "narHash": "sha256-OGcDEz60TXQC+gVz5sdtgGJdKVYr6rwdzQKuZAJQpCA=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3ff0e34b1383648053bba8ed03f201d3466f90c9", "rev": "8f1b52b04f2cb6e5ead50bd28d76528a2f0380ef",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -8,13 +8,12 @@ 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(), container.Nonexistent, argsWt, false, argF, nil, nil) h := helper.NewDirect(t.Context(), "/proc/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,17 +4,20 @@ 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(), container.Nonexistent, argsWt, false, argF, nil, nil) h := helper.New(t.Context(), "/nonexistent", argsWt, false, argF, nil, nil)
wantErr := "container: starting an empty container" wantErr := "sandbox: 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)
@ -33,8 +36,20 @@ 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,6 +38,7 @@ 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(1, ap, sp)) genericStub(flagRestoreFiles(3, ap, sp))
os.Exit(0) os.Exit(0)
} }

View File

@ -1,17 +1,9 @@
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 TestMain(m *testing.M) { func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
helper.InternalHelperStub()
os.Exit(m.Run())
}

View File

@ -1,8 +1,6 @@
package hst package hst
import ( import (
"time"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
) )
@ -12,10 +10,6 @@ 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,10 +62,8 @@ 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,10 +80,8 @@ 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,7 +144,6 @@ 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,7 +71,6 @@ var testCasesPd = []sealTestCase{
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
RetainSession: true, RetainSession: true,
ForwardCancel: true,
}, },
}, },
{ {
@ -221,7 +220,6 @@ 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,10 +32,6 @@ 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,15 +123,7 @@ 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() { go func() { setupErr <- e.Encode(&shimParams{os.Getpid(), seal.container, seal.user.data, hlog.Load()}) }()
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,7 +15,6 @@ import (
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
@ -80,7 +79,6 @@ 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
@ -283,7 +281,6 @@ 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

@ -1,65 +0,0 @@
#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

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

View File

@ -3,13 +3,10 @@ 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"
@ -19,7 +16,55 @@ 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"
@ -28,10 +73,6 @@ 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
@ -41,16 +82,6 @@ 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")
@ -75,63 +106,18 @@ func ShimMain() {
} else { } else {
internal.InstallOutput(params.Verbose) internal.InstallOutput(params.Verbose)
closeSetup = f closeSetup = f
}
var signalPipe io.ReadCloser
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid // 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 { if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(params.Monitor)); 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) 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 // 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 { 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) 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")
} }
@ -162,18 +148,12 @@ 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)
cancelContainer.Store(&stop) defer stop() // unreachable
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 = params.WaitDelay z.WaitDelay = 2 * time.Second
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,6 +5,7 @@ import (
"context" "context"
"io" "io"
"os" "os"
"os/exec"
"time" "time"
"hakurei.app/container" "hakurei.app/container"
@ -18,10 +19,16 @@ var (
msgStaticGlibc = []byte("not a dynamic executable") msgStaticGlibc = []byte("not a dynamic executable")
) )
func Exec(ctx context.Context, p string) ([]*Entry, error) { func Exec(ctx context.Context, p string) ([]*Entry, error) { return ExecFilter(ctx, nil, nil, p) }
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
@ -47,5 +54,8 @@ func Exec(ctx context.Context, p string) ([]*Entry, error) {
} }
v := stdout.Bytes() v := stdout.Bytes()
if f != nil {
v = f(v)
}
return Parse(v) return Parse(v)
} }

View File

@ -82,8 +82,7 @@ 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;
}; };
@ -128,7 +127,6 @@ in
container = { container = {
inherit (app) inherit (app)
wait_delay
devel devel
userns userns
net net
@ -177,7 +175,8 @@ in
auto_etc = true; auto_etc = true;
cover = [ "/var/run/nscd" ]; cover = [ "/var/run/nscd" ];
symlink = [ symlink =
[
[ [
"*/run/current-system" "*/run/current-system"
"/run/current-system" "/run/current-system"

View File

@ -76,7 +76,6 @@ in
type = type =
let let
inherit (types) inherit (types)
int
ints ints
str str
bool bool
@ -196,16 +195,6 @@ 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.2"; version = "0.1.1";
srcFiltered = builtins.path { srcFiltered = builtins.path {
name = "${pname}-src"; name = "${pname}-src";
@ -83,7 +83,8 @@ 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
@ -129,7 +130,8 @@ buildGoModule rec {
} }
''; '';
passthru.targetPkgs = [ passthru.targetPkgs =
[
go go
gcc gcc
xorg.xorgproto xorg.xorgproto

View File

@ -3,7 +3,6 @@ package system
import ( import (
"testing" "testing"
"hakurei.app/container"
"hakurei.app/system/acl" "hakurei.app/system/acl"
) )
@ -53,19 +52,19 @@ func TestACLString(t *testing.T) {
et Enablement et Enablement
perms []acl.Perm perms []acl.Perm
}{ }{
{`--- type: process path: "/proc/nonexistent"`, Process, []acl.Perm{}}, {`--- type: process path: "/nonexistent"`, Process, []acl.Perm{}},
{`r-- type: user path: "/proc/nonexistent"`, User, []acl.Perm{acl.Read}}, {`r-- type: user path: "/nonexistent"`, User, []acl.Perm{acl.Read}},
{`-w- type: wayland path: "/proc/nonexistent"`, EWayland, []acl.Perm{acl.Write}}, {`-w- type: wayland path: "/nonexistent"`, EWayland, []acl.Perm{acl.Write}},
{`--x type: x11 path: "/proc/nonexistent"`, EX11, []acl.Perm{acl.Execute}}, {`--x type: x11 path: "/nonexistent"`, EX11, []acl.Perm{acl.Execute}},
{`rw- type: dbus path: "/proc/nonexistent"`, EDBus, []acl.Perm{acl.Read, acl.Write}}, {`rw- type: dbus path: "/nonexistent"`, EDBus, []acl.Perm{acl.Read, acl.Write}},
{`r-x type: pulseaudio path: "/proc/nonexistent"`, EPulse, []acl.Perm{acl.Read, acl.Execute}}, {`r-x type: pulseaudio path: "/nonexistent"`, EPulse, []acl.Perm{acl.Read, acl.Execute}},
{`rwx type: user path: "/proc/nonexistent"`, User, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, {`rwx type: user path: "/nonexistent"`, User, []acl.Perm{acl.Read, acl.Write, acl.Execute}},
{`rwx type: process path: "/proc/nonexistent"`, Process, []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}}, {`rwx type: process path: "/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: container.Nonexistent} a := &ACL{et: tc.et, perms: tc.perms, path: "/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,17 +1,22 @@
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"
) )
@ -59,23 +64,20 @@ func TestFinalise(t *testing.T) {
} }
func TestProxyStartWaitCloseString(t *testing.T) { func TestProxyStartWaitCloseString(t *testing.T) {
t.Run("sandbox", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, true) }) oldWaitDelay := helper.WaitDelay
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) {
@ -120,13 +122,36 @@ 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()
output := new(strings.Builder)
if !useSandbox { if !useSandbox {
p = dbus.NewDirect(ctx, final, output) p = dbus.NewDirect(ctx, final, nil)
} else { } else {
p = dbus.New(ctx, final, output) 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)
t.Run("invalid wait", func(t *testing.T) { t.Run("invalid wait", func(t *testing.T) {
wantErr := "dbus: not started" wantErr := "dbus: not started"
if err := p.Wait(); err == nil || err.Error() != wantErr { if err := p.Wait(); err == nil || err.Error() != wantErr {
@ -151,9 +176,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 --args=3 --fd=4", os.Args[0]) wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0])
if useSandbox { if useSandbox {
wantSubstr = fmt.Sprintf(`argv: ["%s" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf`, os.Args[0]) wantSubstr = fmt.Sprintf(`argv: ["%s" "-test.run=TestHelperStub" "--" "--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",
@ -178,3 +203,11 @@ 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,6 +36,9 @@ 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
} }
@ -53,7 +56,7 @@ func (p *Proxy) Start() error {
} }
var libPaths []string var libPaths []string
if entries, err := ldd.Exec(ctx, toolPath); err != nil { if entries, err := ldd.ExecFilter(ctx, p.CommandContext, p.FilterF, toolPath); err != nil {
return err return err
} else { } else {
libPaths = ldd.Path(entries) libPaths = ldd.Path(entries)
@ -66,10 +69,15 @@ 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)

View File

@ -1,17 +0,0 @@
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,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"os/exec"
"sync" "sync"
"syscall" "syscall"
@ -36,6 +37,10 @@ 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
} }

9
system/dbus/stub_test.go Normal file
View File

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

View File

@ -3,8 +3,6 @@ package system
import ( import (
"os" "os"
"testing" "testing"
"hakurei.app/container"
) )
func TestEnsure(t *testing.T) { func TestEnsure(t *testing.T) {
@ -62,11 +60,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: container.Nonexistent, path: "/nonexistent",
perm: 0701, perm: 0701,
ephemeral: tc.ephemeral, ephemeral: tc.ephemeral,
} }
want := "mode: " + os.FileMode(0701).String() + " type: " + tc.want + ` path: "/proc/nonexistent"` want := "mode: " + os.FileMode(0701).String() + " type: " + tc.want + " path: \"/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,21 +127,6 @@
}; };
}; };
"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,6 +173,13 @@ 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,6 +199,13 @@ 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,6 +200,13 @@ 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,6 +199,13 @@ 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,6 +200,13 @@ 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,28 +178,9 @@ 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")