Compare commits
13 Commits
6ba19a7ba5
...
cb513bb1cd
Author | SHA1 | Date | |
---|---|---|---|
cb513bb1cd | |||
f7bd28118c | |||
940ee00ffe | |||
b43d104680 | |||
ddf48a6c22 | |||
a0f499e30a | |||
d6b07f12ff | |||
65fe09caf9 | |||
a1e5f020f4 | |||
bd3fa53a55 | |||
625632c593 | |||
e71ae3b8c5 | |||
9d7a19d162 |
@ -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,
|
||||||
|
@ -215,8 +215,7 @@ stdenv.mkDerivation {
|
|||||||
# create binary cache
|
# create binary cache
|
||||||
closureInfo="${
|
closureInfo="${
|
||||||
closureInfo {
|
closureInfo {
|
||||||
rootPaths =
|
rootPaths = [
|
||||||
[
|
|
||||||
homeManagerConfiguration.activationPackage
|
homeManagerConfiguration.activationPackage
|
||||||
launcher
|
launcher
|
||||||
]
|
]
|
||||||
|
@ -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)},
|
||||||
|
@ -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
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
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,49 +260,70 @@ 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) {
|
sig := make(chan os.Signal, 1)
|
||||||
if uid := syscall.Getuid(); uid != 1000 {
|
signal.Notify(sig, os.Interrupt)
|
||||||
t.Errorf("Getuid: %d, want 1000", uid)
|
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
|
||||||
}
|
|
||||||
if gid := syscall.Getgid(); gid != 100 {
|
|
||||||
t.Errorf("Getgid: %d, want 100", gid)
|
|
||||||
}
|
}
|
||||||
|
select {}
|
||||||
})
|
})
|
||||||
t.Run("hostname", func(t *testing.T) {
|
|
||||||
if name, err := os.Hostname(); err != nil {
|
c.Command("container", command.UsageInternal, func(args []string) error {
|
||||||
t.Fatalf("cannot get hostname: %v", err)
|
if len(args) != 1 {
|
||||||
} else if name != os.Args[5] {
|
return syscall.EINVAL
|
||||||
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 {
|
||||||
t.Fatalf("%v", err)
|
return fmt.Errorf("cannot read /etc/hostname: %v", err)
|
||||||
} else if string(p) != os.Args[5] {
|
} else if string(p) != wantHost {
|
||||||
t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
|
return fmt.Errorf("/etc/hostname: %q, want %q", string(p), wantHost)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
t.Run("mount", func(t *testing.T) {
|
{
|
||||||
|
var fail bool
|
||||||
|
|
||||||
var mnt []*vfs.MountInfoEntry
|
var mnt []*vfs.MountInfoEntry
|
||||||
if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
|
if f, err := os.Open(pathWantMnt); err != nil {
|
||||||
t.Fatalf("cannot receive expected mount points: %v", err)
|
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
|
var d *vfs.MountInfoDecoder
|
||||||
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
|
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
|
||||||
t.Fatalf("cannot open mountinfo: %v", err)
|
return fmt.Errorf("cannot open mountinfo: %v", err)
|
||||||
} else {
|
} else {
|
||||||
d = vfs.NewMountInfoDecoder(f)
|
d = vfs.NewMountInfoDecoder(f)
|
||||||
}
|
}
|
||||||
@ -247,8 +331,7 @@ func TestHelperCheckContainer(t *testing.T) {
|
|||||||
i := 0
|
i := 0
|
||||||
for cur := range d.Entries() {
|
for cur := range d.Entries() {
|
||||||
if i == len(mnt) {
|
if i == len(mnt) {
|
||||||
t.Errorf("got more than %d entries", len(mnt))
|
return fmt.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
|
||||||
@ -258,24 +341,28 @@ func TestHelperCheckContainer(t *testing.T) {
|
|||||||
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") {
|
||||||
t.Errorf("[FAIL] %s", cur)
|
fail = true
|
||||||
|
log.Printf("[FAIL] %s", cur)
|
||||||
} else {
|
} else {
|
||||||
t.Logf("[ OK ] %s", cur)
|
log.Printf("[ OK ] %s", cur)
|
||||||
}
|
}
|
||||||
|
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
if err := d.Err(); err != nil {
|
if err := d.Err(); err != nil {
|
||||||
t.Errorf("cannot parse mountinfo: %v", err)
|
return fmt.Errorf("cannot parse mountinfo: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if i != len(mnt) {
|
if i != len(mnt) {
|
||||||
t.Errorf("got %d entries, want %d", 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")
|
|
||||||
}
|
|
||||||
|
@ -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
69
container/init_test.go
Normal 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...)
|
||||||
|
}
|
122
container/ops.go
122
container/ops.go
@ -15,6 +15,9 @@ 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
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
|
@ -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
12
flake.lock
generated
@ -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": {
|
||||||
|
@ -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",
|
||||||
|
@ -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) })
|
|
||||||
}
|
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -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:
|
||||||
|
@ -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:")
|
||||||
|
65
internal/app/shim-signal.c
Normal file
65
internal/app/shim-signal.c
Normal 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;
|
||||||
|
}
|
3
internal/app/shim-signal.h
Normal file
3
internal/app/shim-signal.h
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd);
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(params.Monitor)); err != nil {
|
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)
|
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")
|
||||||
}
|
}
|
||||||
@ -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:")
|
||||||
|
12
ldd/exec.go
12
ldd/exec.go
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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,8 +177,7 @@ 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"
|
||||||
|
11
options.nix
11
options.nix
@ -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";
|
||||||
|
@ -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,8 +83,7 @@ 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
|
||||||
@ -130,8 +129,7 @@ buildGoModule rec {
|
|||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
passthru.targetPkgs =
|
passthru.targetPkgs = [
|
||||||
[
|
|
||||||
go
|
go
|
||||||
gcc
|
gcc
|
||||||
xorg.xorgproto
|
xorg.xorgproto
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
|
@ -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
17
system/dbus/proc_test.go
Normal 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())
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package dbus_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"hakurei.app/helper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
19
test/test.py
19
test/test.py
@ -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")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user