Compare commits

..

No commits in common. "7638a44fa613f23434318bdf136723aa010e8638" and "a40d1827061650b5ab438e1cc10e12c31eb1391b" have entirely different histories.

138 changed files with 1089 additions and 1975 deletions

View File

@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
@ -12,23 +13,19 @@ import (
"strconv"
"sync"
"time"
_ "unsafe"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/message"
"hakurei.app/system/dbus"
)
//go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap
func optionalErrorUnwrap(_ error) error
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var (
flagVerbose bool
flagJSON bool
@ -118,7 +115,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
progPath := shell
if len(args) > 0 {
if p, err := exec.LookPath(args[0]); err != nil {
log.Fatal(optionalErrorUnwrap(err))
log.Fatal(errors.Unwrap(err))
return err
} else if progPath, err = check.NewAbs(p); err != nil {
log.Fatal(err.Error())

View File

@ -7,12 +7,10 @@ import (
"testing"
"hakurei.app/command"
"hakurei.app/message"
"hakurei.app/container"
)
func TestHelp(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
args []string
@ -70,10 +68,8 @@ Flags:
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
out := new(bytes.Buffer)
c := buildCommand(t.Context(), message.NewMsg(nil), new(earlyHardeningErrs), out)
c := buildCommand(t.Context(), container.NewMsg(nil), new(earlyHardeningErrs), out)
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
t.Errorf("Parse: error = %v; want %v",
err, command.ErrHelp)

View File

@ -13,7 +13,6 @@ import (
"syscall"
"hakurei.app/container"
"hakurei.app/message"
)
var (
@ -32,7 +31,7 @@ func main() {
log.SetPrefix("hakurei: ")
log.SetFlags(0)
msg := message.NewMsg(log.Default())
msg := container.NewMsg(log.Default())
early := earlyHardeningErrs{
yamaLSM: container.SetPtracer(0),

View File

@ -10,13 +10,13 @@ import (
"strings"
"syscall"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/message"
)
func tryPath(msg message.Msg, name string) (config *hst.Config) {
func tryPath(msg container.Msg, name string) (config *hst.Config) {
var r io.Reader
config = new(hst.Config)
@ -49,7 +49,7 @@ func tryPath(msg message.Msg, name string) (config *hst.Config) {
return
}
func tryFd(msg message.Msg, name string) io.ReadCloser {
func tryFd(msg container.Msg, name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil {
if !errors.Is(err, strconv.ErrSyntax) {
msg.Verbosef("name cannot be interpreted as int64: %v", err)
@ -68,7 +68,7 @@ func tryFd(msg message.Msg, name string) io.ReadCloser {
}
}
func tryShort(msg message.Msg, name string) (config *hst.Config, entry *state.State) {
func tryShort(msg container.Msg, name string) (config *hst.Config, entry *state.State) {
likePrefix := false
if len(name) <= 32 {
likePrefix = true

View File

@ -11,10 +11,10 @@ import (
"text/tabwriter"
"time"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/message"
)
func printShowSystem(output io.Writer, short, flagJSON bool) {
@ -56,7 +56,7 @@ func printShowInstance(
if err := config.Validate(); err != nil {
valid = false
if m, ok := message.GetMessage(err); ok {
if m, ok := container.GetErrorMessage(err); ok {
mustPrint(output, "Error: "+m+"!\n\n")
}
}

View File

@ -27,8 +27,6 @@ var (
)
func TestPrintShowInstance(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
instance *state.State
@ -51,7 +49,8 @@ Filesystem
autoroot:w:/var/lib/hakurei/base/org.debian
autoetc:/etc/
w+ephemeral(-rwxr-xr-x):/tmp/
w*/nix/store:/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper:/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work:/var/lib/hakurei/base/org.nixos/ro-store
w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store
*/nix/store
/run/current-system@
/run/opengl-driver@
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
@ -131,7 +130,8 @@ Filesystem
autoroot:w:/var/lib/hakurei/base/org.debian
autoetc:/etc/
w+ephemeral(-rwxr-xr-x):/tmp/
w*/nix/store:/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper:/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work:/var/lib/hakurei/base/org.nixos/ro-store
w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store
*/nix/store
/run/current-system@
/run/opengl-driver@
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
@ -290,10 +290,14 @@ App
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/var/lib/hakurei/base/org.nixos/ro-store"
"/mnt-root/nix/.ro-store"
],
"upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
},
{
"type": "link",
@ -440,10 +444,14 @@ App
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/var/lib/hakurei/base/org.nixos/ro-store"
"/mnt-root/nix/.ro-store"
],
"upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
},
{
"type": "link",
@ -489,8 +497,6 @@ App
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
output := new(strings.Builder)
gotValid := printShowInstance(output, testTime, tc.instance, tc.config, tc.short, tc.json)
if got := output.String(); got != tc.want {
@ -505,8 +511,6 @@ App
}
func TestPrintPs(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
entries state.Entries
@ -650,10 +654,14 @@ func TestPrintPs(t *testing.T) {
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/var/lib/hakurei/base/org.nixos/ro-store"
"/mnt-root/nix/.ro-store"
],
"upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
},
{
"type": "link",
@ -704,8 +712,6 @@ func TestPrintPs(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
output := new(strings.Builder)
printPs(output, testTime, stubStore(tc.entries), tc.short, tc.json)
if got := output.String(); got != tc.want {

View File

@ -91,7 +91,7 @@ func (app *appInfo) toHst(pathSet *appPathSet, pathname *check.Absolute, argv []
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsPrivateTmp.Append("app")}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsTmp.Append("app")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus"), Optional: true}},

View File

@ -11,10 +11,10 @@ import (
"syscall"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/message"
)
var (
@ -24,7 +24,7 @@ var (
func main() {
log.SetPrefix("hpkg: ")
log.SetFlags(0)
msg := message.NewMsg(log.Default())
msg := container.NewMsg(log.Default())
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)
@ -162,7 +162,7 @@ func main() {
withCacheDir(ctx, msg, "install", []string{
// export inner bundle path in the environment
"export BUNDLE=" + hst.PrivateTmp + "/bundle",
"export BUNDLE=" + hst.Tmp + "/bundle",
// replace inner /etc
"mkdir -p etc",
"chmod -R +w etc",
@ -309,7 +309,7 @@ func main() {
if a.GPU {
config.Container.Filesystem = append(config.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsPrivateTmp.Append("nixGL")}})
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsTmp.Append("nixGL")}})
appendGPUFilesystem(config)
}

View File

@ -7,10 +7,10 @@ import (
"strconv"
"sync/atomic"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/message"
)
const bash = "bash"
@ -52,7 +52,7 @@ func lookPath(file string) string {
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(msg message.Msg, name string, arg ...string) {
func mustRun(msg container.Msg, name string, arg ...string) {
msg.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr

View File

@ -9,14 +9,14 @@ import (
"os"
"os/exec"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/message"
)
var hakureiPath = internal.MustHakureiPath()
func mustRunApp(ctx context.Context, msg message.Msg, config *hst.Config, beforeFail func()) {
func mustRunApp(ctx context.Context, msg container.Msg, config *hst.Config, beforeFail func()) {
var (
cmd *exec.Cmd
st io.WriteCloser

View File

@ -5,15 +5,15 @@ import (
"os"
"strings"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/message"
)
func withNixDaemon(
ctx context.Context,
msg message.Msg,
msg container.Msg,
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
@ -64,7 +64,7 @@ func withNixDaemon(
func withCacheDir(
ctx context.Context,
msg message.Msg,
msg container.Msg,
action string, command []string, workDir *check.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, msg, &hst.Config{
@ -88,7 +88,7 @@ func withCacheDir(
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsPrivateTmp.Append("bundle")}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID, "cache"), Source: pathSet.cacheDir, Write: true, Ensure: true}},
},
@ -102,7 +102,7 @@ func withCacheDir(
}, dropShell, beforeFail)
}
func mustRunAppDropShell(ctx context.Context, msg message.Msg, config *hst.Config, dropShell bool, beforeFail func()) {
func mustRunAppDropShell(ctx context.Context, msg container.Msg, config *hst.Config, dropShell bool, beforeFail func()) {
if dropShell {
if config.Container != nil {
config.Container.Args = []string{bash, "-l"}

View File

@ -6,46 +6,32 @@ import (
"testing"
)
func TestParseUint32Fast(t *testing.T) {
t.Parallel()
func Test_parseUint32Fast(t *testing.T) {
t.Run("zero-length", func(t *testing.T) {
t.Parallel()
if _, err := parseUint32Fast(""); err == nil || err.Error() != "zero length string" {
t.Errorf(`parseUint32Fast(""): error = %v`, err)
return
}
})
t.Run("overflow", func(t *testing.T) {
t.Parallel()
if _, err := parseUint32Fast("10000000000"); err == nil || err.Error() != "string too long" {
t.Errorf("parseUint32Fast: error = %v", err)
return
}
})
t.Run("invalid byte", func(t *testing.T) {
t.Parallel()
if _, err := parseUint32Fast("meow"); err == nil || err.Error() != "invalid character 'm' at index 0" {
t.Errorf(`parseUint32Fast("meow"): error = %v`, err)
return
}
})
t.Run("full range", func(t *testing.T) {
t.Parallel()
testRange := func(i, end int) {
for ; i < end; i++ {
s := strconv.Itoa(i)
w := i
t.Run("parse "+s, func(t *testing.T) {
t.Parallel()
v, err := parseUint32Fast(s)
if err != nil {
t.Errorf("parseUint32Fast(%q): error = %v",
@ -69,9 +55,7 @@ func TestParseUint32Fast(t *testing.T) {
})
}
func TestParseConfig(t *testing.T) {
t.Parallel()
func Test_parseConfig(t *testing.T) {
testCases := []struct {
name string
puid, want int
@ -87,8 +71,6 @@ func TestParseConfig(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid)
if err == nil && tc.wantErr != "" {
t.Errorf("parseConfig: error = %v; wantErr %q",

View File

@ -7,7 +7,6 @@ import (
)
func TestBuild(t *testing.T) {
t.Parallel()
c := command.New(nil, nil, "test", nil)
stubHandler := func([]string) error { panic("unreachable") }

View File

@ -14,8 +14,6 @@ import (
)
func TestParse(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
buildTree func(wout, wlog io.Writer) command.Command
@ -253,7 +251,6 @@ Commands:
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
wout, wlog := new(bytes.Buffer), new(bytes.Buffer)
c := tc.buildTree(wout, wlog)

View File

@ -6,19 +6,15 @@ import (
)
func TestParseUnreachable(t *testing.T) {
t.Parallel()
// top level bypasses name matching and recursive calls to Parse
// returns when encountering zero-length args
t.Run("zero-length args", func(t *testing.T) {
t.Parallel()
defer checkRecover(t, "Parse", "attempted to parse with zero length args")
_ = newNode(panicWriter{}, nil, " ", " ").Parse(nil)
})
// top level must not have siblings
t.Run("toplevel siblings", func(t *testing.T) {
t.Parallel()
defer checkRecover(t, "Parse", "invalid toplevel state")
n := newNode(panicWriter{}, nil, " ", "")
n.append(newNode(panicWriter{}, nil, " ", " "))
@ -27,7 +23,6 @@ func TestParseUnreachable(t *testing.T) {
// a node with descendents must not have a direct handler
t.Run("sub handle conflict", func(t *testing.T) {
t.Parallel()
defer checkRecover(t, "Parse", "invalid subcommand tree state")
n := newNode(panicWriter{}, nil, " ", " ")
n.adopt(newNode(panicWriter{}, nil, " ", " "))
@ -37,7 +32,6 @@ func TestParseUnreachable(t *testing.T) {
// this would only happen if a node was matched twice
t.Run("parsed flag set", func(t *testing.T) {
t.Parallel()
defer checkRecover(t, "Parse", "invalid set state")
n := newNode(panicWriter{}, nil, " ", "")
set := flag.NewFlagSet("parsed", flag.ContinueOnError)

View File

@ -10,10 +10,7 @@ import (
)
func TestAutoEtcOp(t *testing.T) {
t.Parallel()
t.Run("nonrepeatable", func(t *testing.T) {
t.Parallel()
wantErr := OpRepeatError("autoetc")
if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
@ -283,7 +280,6 @@ func TestAutoEtcOp(t *testing.T) {
})
t.Run("host path rel", func(t *testing.T) {
t.Parallel()
op := &AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"}
wantHostPath := "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"
wantHostRel := ".host/048090b6ed8f9ebb10e275ff5d8c0659"

View File

@ -6,7 +6,6 @@ import (
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/message"
)
func init() { gob.Register(new(AutoRootOp)) }
@ -82,7 +81,7 @@ func (r *AutoRootOp) String() string {
}
// IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot.
func IsAutoRootBindable(msg message.Msg, name string) bool {
func IsAutoRootBindable(msg Msg, name string) bool {
switch name {
case "proc", "dev", "tmp", "mnt", "etc":

View File

@ -8,12 +8,10 @@ import (
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/message"
)
func TestAutoRootOp(t *testing.T) {
t.Run("nonrepeatable", func(t *testing.T) {
t.Parallel()
wantErr := OpRepeatError("autoroot")
if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
@ -181,8 +179,6 @@ func TestAutoRootOp(t *testing.T) {
}
func TestIsAutoRootBindable(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
want bool
@ -199,8 +195,7 @@ func TestIsAutoRootBindable(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var msg message.Msg
var msg Msg
if tc.log {
msg = &kstub{nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),

View File

@ -3,8 +3,6 @@ package container
import "testing"
func TestCapToIndex(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
cap uintptr
@ -16,7 +14,6 @@ func TestCapToIndex(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := capToIndex(tc.cap); got != tc.want {
t.Errorf("capToIndex: %#x, want %#x", got, tc.want)
}
@ -25,8 +22,6 @@ func TestCapToIndex(t *testing.T) {
}
func TestCapToMask(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
cap uintptr
@ -38,7 +33,6 @@ func TestCapToMask(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := capToMask(tc.cap); got != tc.want {
t.Errorf("capToMask: %#x, want %#x", got, tc.want)
}

View File

@ -18,8 +18,6 @@ import (
func unsafeAbs(_ string) *Absolute
func TestAbsoluteError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
@ -29,8 +27,8 @@ func TestAbsoluteError(t *testing.T) {
}{
{"EINVAL", new(AbsoluteError), syscall.EINVAL, true},
{"not EINVAL", new(AbsoluteError), syscall.EBADE, false},
{"ne val", new(AbsoluteError), &AbsoluteError{Pathname: "etc"}, false},
{"equals", &AbsoluteError{Pathname: "etc"}, &AbsoluteError{Pathname: "etc"}, true},
{"ne val", new(AbsoluteError), &AbsoluteError{"etc"}, false},
{"equals", &AbsoluteError{"etc"}, &AbsoluteError{"etc"}, true},
}
for _, tc := range testCases {
@ -40,18 +38,14 @@ func TestAbsoluteError(t *testing.T) {
}
t.Run("string", func(t *testing.T) {
t.Parallel()
want := `path "etc" is not absolute`
if got := (&AbsoluteError{Pathname: "etc"}).Error(); got != want {
if got := (&AbsoluteError{"etc"}).Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
}
func TestNewAbs(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
@ -60,14 +54,12 @@ func TestNewAbs(t *testing.T) {
wantErr error
}{
{"good", "/etc", MustAbs("/etc"), nil},
{"not absolute", "etc", nil, &AbsoluteError{Pathname: "etc"}},
{"zero", "", nil, &AbsoluteError{Pathname: ""}},
{"not absolute", "etc", nil, &AbsoluteError{"etc"}},
{"zero", "", nil, &AbsoluteError{""}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := NewAbs(tc.pathname)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("NewAbs: %#v, want %#v", got, tc.want)
@ -79,8 +71,6 @@ func TestNewAbs(t *testing.T) {
}
t.Run("must", func(t *testing.T) {
t.Parallel()
defer func() {
wantPanic := `path "etc" is not absolute`
@ -95,8 +85,6 @@ func TestNewAbs(t *testing.T) {
func TestAbsoluteString(t *testing.T) {
t.Run("passthrough", func(t *testing.T) {
t.Parallel()
pathname := "/etc"
if got := unsafeAbs(pathname).String(); got != pathname {
t.Errorf("String: %q, want %q", got, pathname)
@ -104,8 +92,6 @@ func TestAbsoluteString(t *testing.T) {
})
t.Run("zero", func(t *testing.T) {
t.Parallel()
defer func() {
wantPanic := "attempted use of zero Absolute"
@ -119,8 +105,6 @@ func TestAbsoluteString(t *testing.T) {
}
func TestAbsoluteIs(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
a, v *Absolute
@ -136,8 +120,6 @@ func TestAbsoluteIs(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.a.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want)
}
@ -151,8 +133,6 @@ type sCheck struct {
}
func TestCodecAbsolute(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
a *Absolute
@ -173,7 +153,7 @@ func TestCodecAbsolute(t *testing.T) {
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
{"not absolute", nil,
&AbsoluteError{Pathname: "etc"},
&AbsoluteError{"etc"},
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
@ -187,18 +167,13 @@ func TestCodecAbsolute(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("gob", func(t *testing.T) {
if tc.gob == "\x00" && tc.sGob == "\x00" {
// these values mark the current test to skip gob
return
}
t.Parallel()
t.Run("encode", func(t *testing.T) {
t.Parallel()
// encode is unchecked
if errors.Is(tc.wantErr, syscall.EINVAL) {
return
@ -235,8 +210,6 @@ func TestCodecAbsolute(t *testing.T) {
})
t.Run("decode", func(t *testing.T) {
t.Parallel()
{
var gotA *Absolute
err := gob.NewDecoder(strings.NewReader(tc.gob)).Decode(&gotA)
@ -271,11 +244,7 @@ func TestCodecAbsolute(t *testing.T) {
})
t.Run("json", func(t *testing.T) {
t.Parallel()
t.Run("marshal", func(t *testing.T) {
t.Parallel()
// marshal is unchecked
if errors.Is(tc.wantErr, syscall.EINVAL) {
return
@ -310,8 +279,6 @@ func TestCodecAbsolute(t *testing.T) {
})
t.Run("unmarshal", func(t *testing.T) {
t.Parallel()
{
var gotA *Absolute
err := json.Unmarshal([]byte(tc.json), &gotA)
@ -347,8 +314,6 @@ func TestCodecAbsolute(t *testing.T) {
}
t.Run("json passthrough", func(t *testing.T) {
t.Parallel()
wantErr := "invalid character ':' looking for beginning of value"
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
@ -357,11 +322,7 @@ func TestCodecAbsolute(t *testing.T) {
}
func TestAbsoluteWrap(t *testing.T) {
t.Parallel()
t.Run("join", func(t *testing.T) {
t.Parallel()
want := "/etc/nix/nix.conf"
if got := MustAbs("/etc").Append("nix", "nix.conf"); got.String() != want {
t.Errorf("Append: %q, want %q", got, want)
@ -369,8 +330,6 @@ func TestAbsoluteWrap(t *testing.T) {
})
t.Run("dir", func(t *testing.T) {
t.Parallel()
want := "/"
if got := MustAbs("/etc").Dir(); got.String() != want {
t.Errorf("Dir: %q, want %q", got, want)
@ -378,8 +337,6 @@ func TestAbsoluteWrap(t *testing.T) {
})
t.Run("sort", func(t *testing.T) {
t.Parallel()
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
got := []*Absolute{MustAbs("/proc"), MustAbs("/sys"), MustAbs("/etc")}
SortAbs(got)
@ -389,8 +346,6 @@ func TestAbsoluteWrap(t *testing.T) {
})
t.Run("compact", func(t *testing.T) {
t.Parallel()
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
if got := CompactAbs([]*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/proc"), MustAbs("/sys")}); !reflect.DeepEqual(got, want) {
t.Errorf("CompactAbs: %#v, want %#v", got, want)

View File

@ -7,8 +7,6 @@ import (
)
func TestEscapeOverlayDataSegment(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
s string
@ -21,8 +19,6 @@ func TestEscapeOverlayDataSegment(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := check.EscapeOverlayDataSegment(tc.s); got != tc.want {
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
}

View File

@ -18,7 +18,6 @@ import (
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/message"
)
const (
@ -53,7 +52,7 @@ type (
cmd *exec.Cmd
ctx context.Context
msg message.Msg
msg Msg
Params
}
@ -397,9 +396,9 @@ func (p *Container) ProcessState() *os.ProcessState {
}
// New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context, msg message.Msg) *Container {
func New(ctx context.Context, msg Msg) *Container {
if msg == nil {
msg = message.NewMsg(nil)
msg = NewMsg(nil)
}
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}
@ -410,7 +409,7 @@ func New(ctx context.Context, msg message.Msg) *Container {
}
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
func NewCommand(ctx context.Context, msg message.Msg, pathname *check.Absolute, name string, args ...string) *Container {
func NewCommand(ctx context.Context, msg Msg, pathname *check.Absolute, name string, args ...string) *Container {
z := New(ctx, msg)
z.Path = pathname
z.Args = append([]string{name}, args...)

View File

@ -26,12 +26,9 @@ import (
"hakurei.app/container/vfs"
"hakurei.app/hst"
"hakurei.app/ldd"
"hakurei.app/message"
)
func TestStartError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
@ -139,8 +136,6 @@ func TestStartError(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.s {
t.Errorf("Error: %q, want %q", got, tc.s)
@ -157,13 +152,13 @@ func TestStartError(t *testing.T) {
})
t.Run("msg", func(t *testing.T) {
if got, ok := message.GetMessage(tc.err); !ok {
if got, ok := container.GetErrorMessage(tc.err); !ok {
if tc.msg != "" {
t.Errorf("GetMessage: err does not implement MessageError")
t.Errorf("GetErrorMessage: err does not implement MessageError")
}
return
} else if got != tc.msg {
t.Errorf("GetMessage: %q, want %q", got, tc.msg)
t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg)
}
})
})
@ -223,10 +218,10 @@ var containerTestCases = []struct {
{"tmpfs", true, false, false, true,
earlyOps(new(container.Ops).
Tmpfs(hst.AbsPrivateTmp, 0, 0755),
Tmpfs(hst.AbsTmp, 0, 0755),
),
earlyMnt(
ent("/", hst.PrivateTmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
),
9, 9, nil, 0, bits.PresetStrict},
@ -280,7 +275,7 @@ var containerTestCases = []struct {
}
return new(container.Ops).
Overlay(hst.AbsPrivateTmp, upper, work, lower0, lower1),
Overlay(hst.AbsTmp, upper, work, lower0, lower1),
context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1),
testVal("lower0"), lower0),
@ -289,7 +284,7 @@ var containerTestCases = []struct {
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
ent("/", hst.Tmp, "rw", "overlay", "overlay",
"rw,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
@ -315,13 +310,13 @@ var containerTestCases = []struct {
}
return new(container.Ops).
OverlayEphemeral(hst.AbsPrivateTmp, lower0, lower1),
OverlayEphemeral(hst.AbsTmp, lower0, lower1),
t.Context()
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
// contains random suffix
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay", ignore),
ent("/", hst.Tmp, "rw", "overlay", "overlay", ignore),
}
},
1 << 3, 1 << 14, nil, 0, bits.PresetStrict},
@ -338,14 +333,14 @@ var containerTestCases = []struct {
}
}
return new(container.Ops).
OverlayReadonly(hst.AbsPrivateTmp, lower0, lower1),
OverlayReadonly(hst.AbsTmp, lower0, lower1),
context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1),
testVal("lower0"), lower0)
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
ent("/", hst.Tmp, "rw", "overlay", "overlay",
"ro,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
@ -356,8 +351,6 @@ var containerTestCases = []struct {
}
func TestContainer(t *testing.T) {
t.Parallel()
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled
wantExitCode := 0
@ -391,8 +384,6 @@ func TestContainer(t *testing.T) {
for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx)
@ -512,7 +503,6 @@ func testContainerCancel(
waitCheck func(t *testing.T, c *container.Container),
) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block")
@ -555,8 +545,7 @@ func testContainerCancel(
}
func TestContainerString(t *testing.T) {
t.Parallel()
msg := message.NewMsg(nil)
msg := container.NewMsg(nil)
c := container.NewCommand(t.Context(), msg, check.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
c.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset(
@ -722,7 +711,7 @@ func TestMain(m *testing.M) {
}
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
msg := message.NewMsg(nil)
msg := container.NewMsg(nil)
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(check.MustAbs(os.Args[0]), absHelperInnerPath, 0)

View File

@ -11,7 +11,6 @@ import (
"syscall"
"hakurei.app/container/seccomp"
"hakurei.app/message"
)
type osFile interface {
@ -38,7 +37,7 @@ type syscallDispatcher interface {
setNoNewPrivs() error
// lastcap provides [LastCap].
lastcap(msg message.Msg) uintptr
lastcap(msg Msg) uintptr
// capset provides capset.
capset(hdrp *capHeader, datap *[2]capData) error
// capBoundingSetDrop provides capBoundingSetDrop.
@ -53,9 +52,9 @@ type syscallDispatcher interface {
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// bindMount provides procPaths.bindMount.
bindMount(msg message.Msg, source, target string, flags uintptr) error
bindMount(msg Msg, source, target string, flags uintptr) error
// remount provides procPaths.remount.
remount(msg message.Msg, target string, flags uintptr) error
remount(msg Msg, target string, flags uintptr) error
// mountTmpfs provides mountTmpfs.
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
// ensureFile provides ensureFile.
@ -123,11 +122,11 @@ type syscallDispatcher interface {
wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)
// printf provides the Printf method of [log.Logger].
printf(msg message.Msg, format string, v ...any)
printf(msg Msg, format string, v ...any)
// fatal provides the Fatal method of [log.Logger]
fatal(msg message.Msg, v ...any)
fatal(msg Msg, v ...any)
// fatalf provides the Fatalf method of [log.Logger]
fatalf(msg message.Msg, format string, v ...any)
fatalf(msg Msg, format string, v ...any)
}
// direct implements syscallDispatcher on the current kernel.
@ -141,7 +140,7 @@ func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
func (direct) lastcap(msg Msg) uintptr { return LastCap(msg) }
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
@ -151,10 +150,10 @@ func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return Receive(key, e, fdp)
}
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
func (direct) bindMount(msg Msg, source, target string, flags uintptr) error {
return hostProc.bindMount(msg, source, target, flags)
}
func (direct) remount(msg message.Msg, target string, flags uintptr) error {
func (direct) remount(msg Msg, target string, flags uintptr) error {
return hostProc.remount(msg, target, flags)
}
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
@ -222,6 +221,6 @@ func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *s
return syscall.Wait4(pid, wstatus, options, rusage)
}
func (direct) printf(msg message.Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) }
func (direct) fatal(msg message.Msg, v ...any) { msg.GetLogger().Fatal(v...) }
func (direct) fatalf(msg message.Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) }
func (direct) printf(msg Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) }
func (direct) fatal(msg Msg, v ...any) { msg.GetLogger().Fatal(v...) }
func (direct) fatalf(msg Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) }

View File

@ -18,7 +18,6 @@ import (
"hakurei.app/container/seccomp"
"hakurei.app/container/stub"
"hakurei.app/message"
)
type opValidTestCase struct {
@ -32,12 +31,10 @@ func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
t.Run("valid", func(t *testing.T) {
t.Helper()
t.Parallel()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
if got := tc.op.Valid(); got != tc.want {
t.Errorf("Valid: %v, want %v", got, tc.want)
@ -58,12 +55,10 @@ func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
t.Run("build", func(t *testing.T) {
t.Helper()
t.Parallel()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
@ -84,12 +79,10 @@ func checkOpIs(t *testing.T, testCases []opIsTestCase) {
t.Run("is", func(t *testing.T) {
t.Helper()
t.Parallel()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
if got := tc.op.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want)
@ -112,12 +105,10 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
t.Run("meta", func(t *testing.T) {
t.Helper()
t.Parallel()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
t.Run("prefix", func(t *testing.T) {
t.Helper()
@ -158,7 +149,6 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
wait4signal := make(chan struct{})
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
@ -192,12 +182,10 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Run("behaviour", func(t *testing.T) {
t.Helper()
t.Parallel()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
k := &kstub{nil, stub.New(t,
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
@ -341,7 +329,7 @@ func (k *kstub) setDumpable(dumpable uintptr) error {
}
func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err }
func (k *kstub) lastcap(msg message.Msg) uintptr {
func (k *kstub) lastcap(msg Msg) uintptr {
k.Helper()
k.checkMsg(msg)
return k.Expects("lastcap").Ret.(uintptr)
@ -421,7 +409,7 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
return
}
func (k *kstub) bindMount(msg message.Msg, source, target string, flags uintptr) error {
func (k *kstub) bindMount(msg Msg, source, target string, flags uintptr) error {
k.Helper()
k.checkMsg(msg)
return k.Expects("bindMount").Error(
@ -430,7 +418,7 @@ func (k *kstub) bindMount(msg message.Msg, source, target string, flags uintptr)
stub.CheckArg(k.Stub, "flags", flags, 2))
}
func (k *kstub) remount(msg message.Msg, target string, flags uintptr) error {
func (k *kstub) remount(msg Msg, target string, flags uintptr) error {
k.Helper()
k.checkMsg(msg)
return k.Expects("remount").Error(
@ -714,7 +702,7 @@ func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage
return
}
func (k *kstub) printf(_ message.Msg, format string, v ...any) {
func (k *kstub) printf(_ Msg, format string, v ...any) {
k.Helper()
if k.Expects("printf").Error(
stub.CheckArg(k.Stub, "format", format, 0),
@ -723,7 +711,7 @@ func (k *kstub) printf(_ message.Msg, format string, v ...any) {
}
}
func (k *kstub) fatal(_ message.Msg, v ...any) {
func (k *kstub) fatal(_ Msg, v ...any) {
k.Helper()
if k.Expects("fatal").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
@ -732,7 +720,7 @@ func (k *kstub) fatal(_ message.Msg, v ...any) {
panic(stub.PanicExit)
}
func (k *kstub) fatalf(_ message.Msg, format string, v ...any) {
func (k *kstub) fatalf(_ Msg, format string, v ...any) {
k.Helper()
if k.Expects("fatalf").Error(
stub.CheckArg(k.Stub, "format", format, 0),
@ -742,7 +730,7 @@ func (k *kstub) fatalf(_ message.Msg, format string, v ...any) {
panic(stub.PanicExit)
}
func (k *kstub) checkMsg(msg message.Msg) {
func (k *kstub) checkMsg(msg Msg) {
k.Helper()
var target *kstub

View File

@ -59,7 +59,6 @@ func messagePrefixP[V any, T interface {
return zeroString, false
}
// MountError wraps errors returned by syscall.Mount.
type MountError struct {
Source, Target, Fstype string
@ -75,7 +74,6 @@ func (e *MountError) Unwrap() error {
return e.Errno
}
func (e *MountError) Message() string { return "cannot " + e.Error() }
func (e *MountError) Error() string {
if e.Flags&syscall.MS_BIND != 0 {
if e.Flags&syscall.MS_REMOUNT != 0 {
@ -92,15 +90,6 @@ func (e *MountError) Error() string {
return "mount " + e.Target + ": " + e.Errno.Error()
}
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
// if it is not nil, or the original value if it is.
func optionalErrorUnwrap(err error) error {
if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
return underlyingErr
}
return err
}
// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback.
func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
var errno syscall.Errno

View File

@ -14,8 +14,6 @@ import (
)
func TestMessageFromError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
@ -56,7 +54,6 @@ func TestMessageFromError(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, ok := messageFromError(tc.err)
if got != tc.want {
t.Errorf("messageFromError: %q, want %q", got, tc.want)
@ -69,8 +66,6 @@ func TestMessageFromError(t *testing.T) {
}
func TestMountError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
@ -116,7 +111,6 @@ func TestMountError(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("is", func(t *testing.T) {
if !errors.Is(tc.err, tc.errno) {
t.Errorf("Is: %#v is not %v", tc.err, tc.errno)
@ -131,7 +125,6 @@ func TestMountError(t *testing.T) {
}
t.Run("zero", func(t *testing.T) {
t.Parallel()
if errors.Is(new(MountError), syscall.Errno(0)) {
t.Errorf("Is: zero MountError unexpected true")
}
@ -139,8 +132,6 @@ func TestMountError(t *testing.T) {
}
func TestErrnoFallback(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
@ -163,7 +154,6 @@ func TestErrnoFallback(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
errno, err := errnoFallback(tc.name, Nonexistent, tc.err)
if errno != tc.wantErrno {
t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno)

View File

@ -3,8 +3,6 @@ package container
import (
"os"
"sync"
"hakurei.app/message"
)
var (
@ -12,7 +10,7 @@ var (
executableOnce sync.Once
)
func copyExecutable(msg message.Msg) {
func copyExecutable(msg Msg) {
if name, err := os.Executable(); err != nil {
msg.BeforeExit()
msg.GetLogger().Fatalf("cannot read executable path: %v", err)
@ -21,7 +19,7 @@ func copyExecutable(msg message.Msg) {
}
}
func MustExecutable(msg message.Msg) string {
func MustExecutable(msg Msg) string {
executableOnce.Do(func() { copyExecutable(msg) })
return executable
}

View File

@ -5,14 +5,13 @@ import (
"testing"
"hakurei.app/container"
"hakurei.app/message"
)
func TestExecutable(t *testing.T) {
t.Parallel()
for i := 0; i < 16; i++ {
if got := container.MustExecutable(message.NewMsg(nil)); got != os.Args[0] {
t.Errorf("MustExecutable: %q, want %q", got, os.Args[0])
if got := container.MustExecutable(container.NewMsg(nil)); got != os.Args[0] {
t.Errorf("MustExecutable: %q, want %q",
got, os.Args[0])
}
}
}

View File

@ -14,7 +14,6 @@ import (
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/message"
)
const (
@ -62,7 +61,7 @@ type (
setupState struct {
nonrepeatable uintptr
*Params
message.Msg
Msg
}
)
@ -96,14 +95,14 @@ type initParams struct {
}
// Init is called by [TryArgv0] if the current process is the container init.
func Init(msg message.Msg) {
func Init(msg Msg) {
if msg == nil {
panic("attempting to call initEntrypoint with nil msg")
}
initEntrypoint(direct{}, msg)
}
func initEntrypoint(k syscallDispatcher, msg message.Msg) {
func initEntrypoint(k syscallDispatcher, msg Msg) {
k.lockOSThread()
if k.getpid() != 1 {
@ -126,7 +125,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatal(msg, "invalid setup descriptor")
}
if errors.Is(err, ErrReceiveEnv) {
k.fatal(msg, setupEnv+" not set")
k.fatal(msg, "HAKUREI_SETUP not set")
}
k.fatalf(msg, "cannot decode init setup payload: %v", err)
@ -178,7 +177,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
lastcap := k.lastcap(msg)
if err := k.mount(zeroString, fhs.Root, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err))
k.fatalf(msg, "cannot make / rslave: %v", err)
}
state := &setupState{Params: &params.Params, Msg: msg}
@ -202,7 +201,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
k.fatalf(msg, "cannot mount intermediate root: %v", optionalErrorUnwrap(err))
k.fatalf(msg, "cannot mount intermediate root: %v", err)
}
if err := k.chdir(intermediateHostPath); err != nil {
k.fatalf(msg, "cannot enter intermediate host path: %v", err)
@ -212,7 +211,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "%v", err)
}
if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil {
k.fatalf(msg, "cannot bind sysroot: %v", optionalErrorUnwrap(err))
k.fatalf(msg, "cannot bind sysroot: %v", err)
}
if err := k.mkdir(hostDir, 0755); err != nil {
@ -246,7 +245,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
// setup requiring host root complete at this point
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
k.fatalf(msg, "cannot make host root rprivate: %v", optionalErrorUnwrap(err))
k.fatalf(msg, "cannot make host root rprivate: %v", err)
}
if err := k.unmount(hostDir, MNT_DETACH); err != nil {
k.fatalf(msg, "cannot unmount host root: %v", err)
@ -449,11 +448,11 @@ const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init".
// If a nil msg is passed, the system logger is used instead.
func TryArgv0(msg message.Msg) {
func TryArgv0(msg Msg) {
if msg == nil {
log.SetPrefix(initName + ": ")
log.SetFlags(0)
msg = message.NewMsg(log.Default())
msg = NewMsg(log.Default())
}
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {

View File

@ -13,7 +13,6 @@ import (
)
func TestInitEntrypoint(t *testing.T) {
t.Parallel()
checkSimple(t, "initEntrypoint", []simpleTestCase{
{"getpid", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
@ -2650,7 +2649,6 @@ func TestInitEntrypoint(t *testing.T) {
}
func TestOpsGrow(t *testing.T) {
t.Parallel()
ops := new(Ops)
ops.Grow(1 << 4)
if got := cap(*ops); got == 0 {

View File

@ -12,8 +12,6 @@ import (
)
func TestBindMountOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"ENOENT not optional", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
@ -166,10 +164,7 @@ func TestBindMountOp(t *testing.T) {
})
t.Run("unreachable", func(t *testing.T) {
t.Parallel()
t.Run("nil sourceFinal not optional", func(t *testing.T) {
t.Parallel()
wantErr := OpStateError("bind")
if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)

View File

@ -9,8 +9,6 @@ import (
)
func TestMountDevOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"mountTmpfs", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),

View File

@ -9,8 +9,6 @@ import (
)
func TestMkdirOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"success", new(Params), &MkdirOp{
Path: check.MustAbs("/.hakurei"),

View File

@ -10,11 +10,7 @@ import (
)
func TestMountOverlayOp(t *testing.T) {
t.Parallel()
t.Run("argument error", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err *OverlayArgumentError
@ -34,7 +30,6 @@ func TestMountOverlayOp(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want)
}
@ -275,10 +270,7 @@ func TestMountOverlayOp(t *testing.T) {
})
t.Run("unreachable", func(t *testing.T) {
t.Parallel()
t.Run("nil Upper non-nil Work not ephemeral", func(t *testing.T) {
t.Parallel()
wantErr := OpStateError("overlay")
if err := (&MountOverlayOp{
Work: check.MustAbs("/"),

View File

@ -22,6 +22,15 @@ func (f *Ops) Place(name *check.Absolute, data []byte) *Ops {
return f
}
// PlaceP is like Place but writes the address of [TmpfileOp.Data] to the pointer dataP points to.
func (f *Ops) PlaceP(name *check.Absolute, dataP **[]byte) *Ops {
t := &TmpfileOp{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}
// TmpfileOp places a file on container Path containing Data.
type TmpfileOp struct {
Path *check.Absolute

View File

@ -14,7 +14,6 @@ func TestTmpfileOp(t *testing.T) {
samplePath = check.MustAbs("/etc/passwd")
sampleData = []byte(sampleDataString)
)
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"createTemp", &Params{ParentPerm: 0700}, &TmpfileOp{
@ -83,8 +82,18 @@ func TestTmpfileOp(t *testing.T) {
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"full", new(Ops).Place(samplePath, sampleData), Ops{
&TmpfileOp{Path: samplePath, Data: sampleData},
{"noref", new(Ops).Place(samplePath, sampleData), Ops{
&TmpfileOp{
Path: samplePath,
Data: sampleData,
},
}},
{"ref", new(Ops).PlaceP(samplePath, new(*[]byte)), Ops{
&TmpfileOp{
Path: samplePath,
Data: []byte{},
},
}},
})

View File

@ -9,8 +9,6 @@ import (
)
func TestMountProcOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdir", &Params{ParentPerm: 0755},
&MountProcOp{

View File

@ -9,8 +9,6 @@ import (
)
func TestRemountOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"success", new(Params), &RemountOp{
Target: check.MustAbs("/"),

View File

@ -9,8 +9,6 @@ import (
)
func TestSymlinkOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: check.MustAbs("/etc/nixos"),

View File

@ -10,10 +10,7 @@ import (
)
func TestMountTmpfsOp(t *testing.T) {
t.Parallel()
t.Run("size error", func(t *testing.T) {
t.Parallel()
tmpfsSizeError := TmpfsSizeError(-1)
want := "tmpfs size -1 out of bounds"
if got := tmpfsSizeError.Error(); got != want {

View File

@ -8,8 +8,6 @@ import (
)
func TestLandlockString(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
rulesetAttr *container.RulesetAttr
@ -48,7 +46,6 @@ func TestLandlockString(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.rulesetAttr.String(); got != tc.want {
t.Errorf("String: %s, want %s", got, tc.want)
}
@ -57,7 +54,6 @@ func TestLandlockString(t *testing.T) {
}
func TestLandlockAttrSize(t *testing.T) {
t.Parallel()
want := 24
if got := unsafe.Sizeof(container.RulesetAttr{}); got != uintptr(want) {
t.Errorf("Sizeof: %d, want %d", got, want)

View File

@ -7,7 +7,6 @@ import (
. "syscall"
"hakurei.app/container/vfs"
"hakurei.app/message"
)
/*
@ -88,7 +87,7 @@ const (
)
// bindMount mounts source on target and recursively applies flags if MS_REC is set.
func (p *procPaths) bindMount(msg message.Msg, source, target string, flags uintptr) error {
func (p *procPaths) bindMount(msg Msg, source, target string, flags uintptr) error {
// syscallDispatcher.bindMount and procPaths.remount must not be called from this function
if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
@ -98,7 +97,7 @@ func (p *procPaths) bindMount(msg message.Msg, source, target string, flags uint
}
// remount applies flags on target, recursively if MS_REC is set.
func (p *procPaths) remount(msg message.Msg, target string, flags uintptr) error {
func (p *procPaths) remount(msg Msg, target string, flags uintptr) error {
// syscallDispatcher methods bindMount, remount must not be called from this function
var targetFinal string
@ -160,7 +159,7 @@ func (p *procPaths) remount(msg message.Msg, target string, flags uintptr) error
}
// remountWithFlags remounts mount point described by [vfs.MountInfoNode].
func remountWithFlags(k syscallDispatcher, msg message.Msg, n *vfs.MountInfoNode, mf uintptr) error {
func remountWithFlags(k syscallDispatcher, msg Msg, n *vfs.MountInfoNode, mf uintptr) error {
// syscallDispatcher methods bindMount, remount must not be called from this function
kf, unmatched := n.Flags()

View File

@ -10,8 +10,6 @@ import (
)
func TestBindMount(t *testing.T) {
t.Parallel()
checkSimple(t, "bindMount", []simpleTestCase{
{"mount", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount(nil, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
@ -36,8 +34,6 @@ func TestBindMount(t *testing.T) {
}
func TestRemount(t *testing.T) {
t.Parallel()
const sampleMountinfoNix = `254 407 253:0 / /host rw,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
255 254 0:28 / /host/mnt/.ro-cwd ro,noatime master:2 - 9p cwd ro,access=client,msize=16384,trans=virtio
256 254 0:29 / /host/nix/.ro-store rw,relatime master:3 - 9p nix-store rw,cache=f,access=client,msize=16384,trans=virtio
@ -220,8 +216,6 @@ func TestRemount(t *testing.T) {
}
func TestRemountWithFlags(t *testing.T) {
t.Parallel()
checkSimple(t, "remountWithFlags", []simpleTestCase{
{"noop unmatched", func(k *kstub) error {
return remountWithFlags(k, k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
@ -242,8 +236,6 @@ func TestRemountWithFlags(t *testing.T) {
}
func TestMountTmpfs(t *testing.T) {
t.Parallel()
checkSimple(t, "mountTmpfs", []simpleTestCase{
{"mkdirAll", func(k *kstub) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
@ -268,8 +260,6 @@ func TestMountTmpfs(t *testing.T) {
}
func TestParentPerm(t *testing.T) {
t.Parallel()
testCases := []struct {
perm os.FileMode
want os.FileMode
@ -285,7 +275,6 @@ func TestParentPerm(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.perm.String(), func(t *testing.T) {
t.Parallel()
if got := parentPerm(tc.perm); got != tc.want {
t.Errorf("parentPerm: %#o, want %#o", got, tc.want)
}

View File

@ -1,5 +1,4 @@
// Package message provides interfaces and a base implementation for extended reporting on top of [log.Logger]
package message
package container
import (
"errors"
@ -7,19 +6,19 @@ import (
"sync/atomic"
)
// Error is an error with a user-facing message.
type Error interface {
// MessageError is an error with a user-facing message.
type MessageError interface {
// Message returns a user-facing error message.
Message() string
error
}
// GetMessage returns whether an error implements [Error], and the message if it does.
func GetMessage(err error) (string, bool) {
var e Error
// GetErrorMessage returns whether an error implements [MessageError], and the message if it does.
func GetErrorMessage(err error) (string, bool) {
var e MessageError
if !errors.As(err, &e) || e == nil {
return "", false
return zeroString, false
}
return e.Message(), true
}

View File

@ -1,4 +1,4 @@
package message_test
package container_test
import (
"bytes"
@ -11,12 +11,9 @@ import (
"hakurei.app/container"
"hakurei.app/container/stub"
"hakurei.app/message"
)
func TestMessageError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
@ -32,13 +29,12 @@ func TestMessageError(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, ok := message.GetMessage(tc.err)
got, ok := container.GetErrorMessage(tc.err)
if got != tc.want {
t.Errorf("GetMessage: %q, want %q", got, tc.want)
t.Errorf("GetErrorMessage: %q, want %q", got, tc.want)
}
if ok != tc.wantOk {
t.Errorf("GetMessage: ok = %v, want %v", ok, tc.wantOk)
t.Errorf("GetErrorMessage: ok = %v, want %v", ok, tc.wantOk)
}
})
}
@ -47,13 +43,12 @@ func TestMessageError(t *testing.T) {
func TestDefaultMsg(t *testing.T) {
// copied from output.go
const suspendBufMax = 1 << 24
t.Parallel()
t.Run("logger", func(t *testing.T) {
t.Run("nil", func(t *testing.T) {
got := message.NewMsg(nil).GetLogger()
got := container.NewMsg(nil).GetLogger()
if out := got.Writer().(*message.Suspendable).Downstream; out != log.Writer() {
if out := got.Writer().(*container.Suspendable).Downstream; out != log.Writer() {
t.Errorf("GetLogger: Downstream = %#v", out)
}
@ -64,13 +59,13 @@ func TestDefaultMsg(t *testing.T) {
t.Run("takeover", func(t *testing.T) {
l := log.New(io.Discard, "\x00", 0xdeadbeef)
got := message.NewMsg(l)
got := container.NewMsg(l)
if logger := got.GetLogger(); logger != l {
t.Errorf("GetLogger: %#v, want %#v", logger, l)
}
if ds := l.Writer().(*message.Suspendable).Downstream; ds != io.Discard {
if ds := l.Writer().(*container.Suspendable).Downstream; ds != io.Discard {
t.Errorf("GetLogger: Downstream = %#v", ds)
}
})
@ -83,93 +78,93 @@ func TestDefaultMsg(t *testing.T) {
pt, next []byte
err error
f func(t *testing.T, msg message.Msg)
f func(t *testing.T, msg container.Msg)
}{
{"zero verbose", nil, nil, nil, func(t *testing.T, msg message.Msg) {
{"zero verbose", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if msg.IsVerbose() {
t.Error("IsVerbose unexpected true")
}
}},
{"swap false", nil, nil, nil, func(t *testing.T, msg message.Msg) {
{"swap false", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if msg.SwapVerbose(false) {
t.Error("SwapVerbose unexpected true")
}
}},
{"write discard", nil, nil, nil, func(_ *testing.T, msg message.Msg) {
{"write discard", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.Verbose("\x00")
msg.Verbosef("\x00")
}},
{"verbose false", nil, nil, nil, func(t *testing.T, msg message.Msg) {
{"verbose false", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if msg.IsVerbose() {
t.Error("IsVerbose unexpected true")
}
}},
{"swap true", nil, nil, nil, func(t *testing.T, msg message.Msg) {
{"swap true", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if msg.SwapVerbose(true) {
t.Error("SwapVerbose unexpected true")
}
}},
{"write verbose", []byte("test: \x00\n"), nil, nil, func(_ *testing.T, msg message.Msg) {
{"write verbose", []byte("test: \x00\n"), nil, nil, func(_ *testing.T, msg container.Msg) {
msg.Verbose("\x00")
}},
{"write verbosef", []byte(`test: "\x00"` + "\n"), nil, nil, func(_ *testing.T, msg message.Msg) {
{"write verbosef", []byte(`test: "\x00"` + "\n"), nil, nil, func(_ *testing.T, msg container.Msg) {
msg.Verbosef("%q", "\x00")
}},
{"verbose true", nil, nil, nil, func(t *testing.T, msg message.Msg) {
{"verbose true", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if !msg.IsVerbose() {
t.Error("IsVerbose unexpected false")
}
}},
{"resume noop", nil, nil, nil, func(t *testing.T, msg message.Msg) {
{"resume noop", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if msg.Resume() {
t.Error("Resume unexpected success")
}
}},
{"beforeExit noop", nil, nil, nil, func(_ *testing.T, msg message.Msg) {
{"beforeExit noop", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.BeforeExit()
}},
{"beforeExit suspend", nil, nil, nil, func(_ *testing.T, msg message.Msg) {
{"beforeExit suspend", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.Suspend()
}},
{"beforeExit message", []byte("test: beforeExit reached on suspended output\n"), nil, nil, func(_ *testing.T, msg message.Msg) {
{"beforeExit message", []byte("test: beforeExit reached on suspended output\n"), nil, nil, func(_ *testing.T, msg container.Msg) {
msg.BeforeExit()
}},
{"post beforeExit resume noop", nil, nil, nil, func(t *testing.T, msg message.Msg) {
{"post beforeExit resume noop", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if msg.Resume() {
t.Error("Resume unexpected success")
}
}},
{"suspend", nil, nil, nil, func(_ *testing.T, msg message.Msg) {
{"suspend", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.Suspend()
}},
{"suspend write", nil, nil, nil, func(_ *testing.T, msg message.Msg) {
{"suspend write", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.GetLogger().Print("\x00")
}},
{"resume error", []byte("test: \x00\n"), []byte("test: cannot dump buffer on resume: unique error 0 injected by the test suite\n"), stub.UniqueError(0), func(t *testing.T, msg message.Msg) {
{"resume error", []byte("test: \x00\n"), []byte("test: cannot dump buffer on resume: unique error 0 injected by the test suite\n"), stub.UniqueError(0), func(t *testing.T, msg container.Msg) {
if !msg.Resume() {
t.Error("Resume unexpected failure")
}
}},
{"suspend drop", nil, nil, nil, func(_ *testing.T, msg message.Msg) {
{"suspend drop", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.Suspend()
}},
{"suspend write fill", nil, nil, nil, func(_ *testing.T, msg message.Msg) {
{"suspend write fill", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.GetLogger().Print(strings.Repeat("\x00", suspendBufMax))
}},
{"resume dropped", append([]byte("test: "), bytes.Repeat([]byte{0}, suspendBufMax-6)...), []byte("test: dropped 7 bytes while output is suspended\n"), nil, func(t *testing.T, msg message.Msg) {
{"resume dropped", append([]byte("test: "), bytes.Repeat([]byte{0}, suspendBufMax-6)...), []byte("test: dropped 7 bytes while output is suspended\n"), nil, func(t *testing.T, msg container.Msg) {
if !msg.Resume() {
t.Error("Resume unexpected failure")
}
}},
}
msg := message.NewMsg(log.New(&dw, "test: ", 0))
msg := container.NewMsg(log.New(&dw, "test: ", 0))
for _, step := range steps {
// these share the same writer, so cannot be subtests
t.Logf("running step %q", step.name)

View File

@ -1,4 +1,4 @@
package message
package container
import (
"bytes"

View File

@ -1,4 +1,4 @@
package message_test
package container_test
import (
"bytes"
@ -8,14 +8,13 @@ import (
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/container/stub"
"hakurei.app/message"
)
func TestSuspendable(t *testing.T) {
// copied from output.go
const suspendBufMax = 1 << 24
t.Parallel()
const (
// equivalent to len(want.pt)
@ -75,7 +74,7 @@ func TestSuspendable(t *testing.T) {
var dw expectWriter
w := message.Suspendable{Downstream: &dw}
w := container.Suspendable{Downstream: &dw}
for _, step := range steps {
// these share the same writer, so cannot be subtests
t.Logf("writing step %q", step.name)

View File

@ -31,7 +31,7 @@ func Receive(key string, e any, fdp *uintptr) (func() error, error) {
return nil, ErrReceiveEnv
} else {
if fd, err := strconv.Atoi(s); err != nil {
return nil, optionalErrorUnwrap(err)
return nil, errors.Unwrap(err)
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {

View File

@ -117,7 +117,7 @@ func Export(fd int, rules []NativeRule, flags ExportFlag) error {
var ret C.int
var rulesPinner runtime.Pinner
rulesPinner := new(runtime.Pinner)
for i := range rules {
rule := &rules[i]
rulesPinner.Pin(rule)
@ -189,5 +189,6 @@ func syscallResolveName(s string) (trap int) {
v := C.CString(s)
trap = int(C.seccomp_syscall_resolve_name(v))
C.free(unsafe.Pointer(v))
return
}

View File

@ -13,8 +13,6 @@ import (
)
func TestExport(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
flags ExportFlag
@ -34,15 +32,14 @@ func TestExport(t *testing.T) {
{"hakurei tty", 0, PresetExt | PresetDenyNS | PresetDenyDevel, false},
}
buf := make([]byte, 8)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
e := New(Preset(tc.presets, tc.flags), tc.flags)
want := bpfExpected[bpfPreset{tc.flags, tc.presets}]
digest := sha512.New()
if _, err := io.Copy(digest, e); (err != nil) != tc.wantErr {
if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr {
t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr)
return
}
@ -50,7 +47,7 @@ func TestExport(t *testing.T) {
t.Errorf("Close: error = %v", err)
}
if got := digest.Sum(nil); !slices.Equal(got, want) {
t.Fatalf("Export: hash = %x, want %x",
t.Fatalf("Export() hash = %x, want %x",
got, want)
return
}

View File

@ -21,7 +21,9 @@ Methods of Encoder are not safe for concurrent use.
An Encoder must not be copied after first use.
*/
type Encoder struct{ *exporter }
type Encoder struct {
*exporter
}
func (e *Encoder) Read(p []byte) (n int, err error) {
if err = e.prepare(); err != nil {

View File

@ -10,8 +10,6 @@ import (
)
func TestLibraryError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
sample *seccomp.LibraryError
@ -43,8 +41,6 @@ func TestLibraryError(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if errors.Is(tc.sample, tc.compare) != tc.wantIs {
t.Errorf("errors.Is(%#v, %#v) did not return %v",
tc.sample, tc.compare, tc.wantIs)
@ -58,8 +54,6 @@ func TestLibraryError(t *testing.T) {
}
t.Run("invalid", func(t *testing.T) {
t.Parallel()
wantPanic := "invalid libseccomp error"
defer func() {
if r := recover(); r != wantPanic {

View File

@ -5,17 +5,15 @@ import (
)
func TestSyscallResolveName(t *testing.T) {
t.Parallel()
for name, want := range Syscalls() {
t.Run(name, func(t *testing.T) {
t.Parallel()
if got := syscallResolveName(name); got != want {
t.Errorf("syscallResolveName(%q) = %d, want %d", name, got, want)
t.Errorf("syscallResolveName(%q) = %d, want %d",
name, got, want)
}
if got, ok := SyscallResolveName(name); !ok || got != want {
t.Errorf("SyscallResolveName(%q) = %d, want %d", name, got, want)
t.Errorf("SyscallResolveName(%q) = %d, want %d",
name, got, want)
}
})
}

View File

@ -8,17 +8,13 @@ import (
)
func TestCallError(t *testing.T) {
t.Parallel()
t.Run("contains false", func(t *testing.T) {
t.Parallel()
if err := new(stub.Call).Error(true, false, true); !reflect.DeepEqual(err, stub.ErrCheck) {
t.Errorf("Error: %#v, want %#v", err, stub.ErrCheck)
}
})
t.Run("passthrough", func(t *testing.T) {
t.Parallel()
wantErr := stub.UniqueError(0xbabe)
if err := (&stub.Call{Err: wantErr}).Error(true); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Error: %#v, want %#v", err, wantErr)

View File

@ -9,10 +9,7 @@ import (
)
func TestUniqueError(t *testing.T) {
t.Parallel()
t.Run("format", func(t *testing.T) {
t.Parallel()
want := "unique error 2989 injected by the test suite"
if got := stub.UniqueError(0xbad).Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
@ -20,17 +17,13 @@ func TestUniqueError(t *testing.T) {
})
t.Run("is", func(t *testing.T) {
t.Parallel()
t.Run("type", func(t *testing.T) {
t.Parallel()
if errors.Is(stub.UniqueError(0), syscall.ENOTRECOVERABLE) {
t.Error("Is: unexpected true")
}
})
t.Run("val", func(t *testing.T) {
t.Parallel()
if errors.Is(stub.UniqueError(0), stub.UniqueError(1)) {
t.Error("Is: unexpected true")
}

View File

@ -32,20 +32,13 @@ func (o *overrideTFailNow) Fail() {
}
func TestHandleExit(t *testing.T) {
t.Parallel()
t.Run("exit", func(t *testing.T) {
t.Parallel()
defer stub.HandleExit(t)
panic(stub.PanicExit)
})
t.Run("goexit", func(t *testing.T) {
t.Parallel()
t.Run("FailNow", func(t *testing.T) {
t.Parallel()
ot := &overrideTFailNow{T: t}
defer func() {
if !ot.failNow {
@ -57,8 +50,6 @@ func TestHandleExit(t *testing.T) {
})
t.Run("Fail", func(t *testing.T) {
t.Parallel()
ot := &overrideTFailNow{T: t}
defer func() {
if !ot.fail {
@ -71,16 +62,11 @@ func TestHandleExit(t *testing.T) {
})
t.Run("nil", func(t *testing.T) {
t.Parallel()
defer stub.HandleExit(t)
})
t.Run("passthrough", func(t *testing.T) {
t.Parallel()
t.Run("toplevel", func(t *testing.T) {
t.Parallel()
defer func() {
want := 0xcafebabe
if r := recover(); r != want {
@ -93,8 +79,6 @@ func TestHandleExit(t *testing.T) {
})
t.Run("new", func(t *testing.T) {
t.Parallel()
defer func() {
want := 0xcafe
if r := recover(); r != want {

View File

@ -45,17 +45,12 @@ func New[K any](tb testing.TB, makeK func(s *Stub[K]) K, want Expect) *Stub[K] {
return &Stub[K]{TB: tb, makeK: makeK, want: want, wg: new(sync.WaitGroup)}
}
func (s *Stub[K]) FailNow() { s.Helper(); panic(panicFailNow) }
func (s *Stub[K]) Fatal(args ...any) { s.Helper(); s.Error(args...); panic(panicFatal) }
func (s *Stub[K]) Fatalf(format string, args ...any) {
s.Helper()
s.Errorf(format, args...)
panic(panicFatalf)
}
func (s *Stub[K]) SkipNow() { s.Helper(); panic("invalid call to SkipNow") }
func (s *Stub[K]) Skip(...any) { s.Helper(); panic("invalid call to Skip") }
func (s *Stub[K]) Skipf(string, ...any) { s.Helper(); panic("invalid call to Skipf") }
func (s *Stub[K]) FailNow() { panic(panicFailNow) }
func (s *Stub[K]) Fatal(args ...any) { s.Error(args...); panic(panicFatal) }
func (s *Stub[K]) Fatalf(format string, args ...any) { s.Errorf(format, args...); panic(panicFatalf) }
func (s *Stub[K]) SkipNow() { panic("invalid call to SkipNow") }
func (s *Stub[K]) Skip(...any) { panic("invalid call to Skip") }
func (s *Stub[K]) Skipf(string, ...any) { panic("invalid call to Skipf") }
// New calls f in a new goroutine
func (s *Stub[K]) New(f func(k K)) {

View File

@ -36,65 +36,49 @@ func (t *overrideT) Errorf(format string, args ...any) {
}
func TestStub(t *testing.T) {
t.Parallel()
t.Run("goexit", func(t *testing.T) {
t.Parallel()
t.Run("FailNow", func(t *testing.T) {
t.Parallel()
defer func() {
if r := recover(); r != panicFailNow {
t.Errorf("recover: %v", r)
}
}()
stubHolder{&Stub[stubHolder]{TB: t}}.FailNow()
new(stubHolder).FailNow()
})
t.Run("SkipNow", func(t *testing.T) {
t.Parallel()
defer func() {
want := "invalid call to SkipNow"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
stubHolder{&Stub[stubHolder]{TB: t}}.SkipNow()
new(stubHolder).SkipNow()
})
t.Run("Skip", func(t *testing.T) {
t.Parallel()
defer func() {
want := "invalid call to Skip"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
stubHolder{&Stub[stubHolder]{TB: t}}.Skip()
new(stubHolder).Skip()
})
t.Run("Skipf", func(t *testing.T) {
t.Parallel()
defer func() {
want := "invalid call to Skipf"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
stubHolder{&Stub[stubHolder]{TB: t}}.Skipf("")
new(stubHolder).Skipf("")
})
})
t.Run("new", func(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) {
t.Parallel()
s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
{"New", ExpectArgs{}, nil, nil},
}, Tracks: []Expect{{Calls: []Call{
@ -128,8 +112,6 @@ func TestStub(t *testing.T) {
})
t.Run("overrun", func(t *testing.T) {
t.Parallel()
ot := &overrideT{T: t}
ot.error.Store(checkError(t, "New: track overrun"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
@ -153,11 +135,7 @@ func TestStub(t *testing.T) {
})
t.Run("expects", func(t *testing.T) {
t.Parallel()
t.Run("overrun", func(t *testing.T) {
t.Parallel()
ot := &overrideT{T: t}
ot.error.Store(checkError(t, "Expects: advancing beyond expected calls"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
@ -165,11 +143,7 @@ func TestStub(t *testing.T) {
})
t.Run("separator", func(t *testing.T) {
t.Parallel()
t.Run("overrun", func(t *testing.T) {
t.Parallel()
ot := &overrideT{T: t}
ot.errorf.Store(checkErrorf(t, "Expects: func = %s, separator overrun", "meow"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
@ -179,8 +153,6 @@ func TestStub(t *testing.T) {
})
t.Run("mismatch", func(t *testing.T) {
t.Parallel()
ot := &overrideT{T: t}
ot.errorf.Store(checkErrorf(t, "Expects: separator, want %s", "panic"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
@ -191,8 +163,6 @@ func TestStub(t *testing.T) {
})
t.Run("mismatch", func(t *testing.T) {
t.Parallel()
ot := &overrideT{T: t}
ot.errorf.Store(checkErrorf(t, "Expects: func = %s, want %s", "meow", "nya"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
@ -206,8 +176,6 @@ func TestStub(t *testing.T) {
func TestCheckArg(t *testing.T) {
t.Run("oob negative", func(t *testing.T) {
t.Parallel()
defer func() {
want := "invalid call to CheckArg"
if r := recover(); r != want {
@ -223,14 +191,12 @@ func TestCheckArg(t *testing.T) {
{"panic", ExpectArgs{PanicExit}, nil, nil},
{"meow", ExpectArgs{-1}, nil, nil},
}})
t.Run("match", func(t *testing.T) {
s.Expects("panic")
if !CheckArg(s, "v", PanicExit, 0) {
t.Errorf("CheckArg: unexpected false")
}
})
t.Run("mismatch", func(t *testing.T) {
defer HandleExit(t)
s.Expects("meow")
@ -239,7 +205,6 @@ func TestCheckArg(t *testing.T) {
t.Errorf("CheckArg: unexpected true")
}
})
t.Run("oob", func(t *testing.T) {
s.pos++
defer func() {
@ -253,11 +218,7 @@ func TestCheckArg(t *testing.T) {
}
func TestCheckArgReflect(t *testing.T) {
t.Parallel()
t.Run("oob lower", func(t *testing.T) {
t.Parallel()
defer func() {
want := "invalid call to CheckArgReflect"
if r := recover(); r != want {

View File

@ -7,7 +7,6 @@ import (
"sync"
"hakurei.app/container/fhs"
"hakurei.app/message"
)
var (
@ -24,7 +23,7 @@ const (
kernelCapLastCapPath = fhs.ProcSys + "kernel/cap_last_cap"
)
func mustReadSysctl(msg message.Msg) {
func mustReadSysctl(msg Msg) {
sysctlOnce.Do(func() {
if v, err := os.ReadFile(kernelOverflowuidPath); err != nil {
msg.GetLogger().Fatalf("cannot read %q: %v", kernelOverflowuidPath, err)
@ -46,6 +45,6 @@ func mustReadSysctl(msg message.Msg) {
})
}
func OverflowUid(msg message.Msg) int { mustReadSysctl(msg); return kernelOverflowuid }
func OverflowGid(msg message.Msg) int { mustReadSysctl(msg); return kernelOverflowgid }
func LastCap(msg message.Msg) uintptr { mustReadSysctl(msg); return uintptr(kernelCapLastCap) }
func OverflowUid(msg Msg) int { mustReadSysctl(msg); return kernelOverflowuid }
func OverflowGid(msg Msg) int { mustReadSysctl(msg); return kernelOverflowgid }
func LastCap(msg Msg) uintptr { mustReadSysctl(msg); return uintptr(kernelCapLastCap) }

View File

@ -7,8 +7,6 @@ import (
)
func TestUnmangle(t *testing.T) {
t.Parallel()
testCases := []struct {
want string
sample string
@ -19,7 +17,6 @@ func TestUnmangle(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
t.Parallel()
got := vfs.Unmangle(tc.sample)
if got != tc.want {
t.Errorf("Unmangle: %q, want %q",

View File

@ -17,8 +17,6 @@ import (
)
func TestDecoderError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err *vfs.DecoderError
@ -37,17 +35,13 @@ func TestDecoderError(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("error", func(t *testing.T) {
t.Parallel()
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %s, want %s", got, tc.want)
}
})
t.Run("is", func(t *testing.T) {
t.Parallel()
if !errors.Is(tc.err, tc.target) {
t.Errorf("Is: unexpected false")
}
@ -60,8 +54,6 @@ func TestDecoderError(t *testing.T) {
}
func TestMountInfo(t *testing.T) {
t.Parallel()
testCases := []mountInfoTest{
{"count", sampleMountinfoBase + `
21 20 0:53/ /mnt/test rw,relatime - tmpfs rw
@ -195,10 +187,7 @@ id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("decode", func(t *testing.T) {
t.Parallel()
var got *vfs.MountInfo
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
err := d.Decode(&got)
@ -217,7 +206,6 @@ id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
})
t.Run("iter", func(t *testing.T) {
t.Parallel()
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
tc.check(t, d, "Entries",
d.Entries(), d.Err)
@ -229,7 +217,6 @@ id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
})
t.Run("yield", func(t *testing.T) {
t.Parallel()
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
v := false
d.Entries()(func(entry *vfs.MountInfoEntry) bool { v = !v; return v })

View File

@ -10,8 +10,6 @@ import (
)
func TestUnfold(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
sample string
@ -52,8 +50,6 @@ func TestUnfold(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
got, err := d.Unfold(tc.target)

View File

@ -11,29 +11,27 @@ import (
)
func TestArgsString(t *testing.T) {
t.Parallel()
wantString := strings.Join(wantArgs, " ")
if got := argsWt.(fmt.Stringer).String(); got != wantString {
t.Errorf("String: %q, want %q", got, wantString)
t.Errorf("String: %q, want %q",
got, wantString)
}
}
func TestNewCheckedArgs(t *testing.T) {
t.Parallel()
args := []string{"\x00"}
if _, err := helper.NewCheckedArgs(args); !errors.Is(err, syscall.EINVAL) {
t.Errorf("NewCheckedArgs: error = %v, wantErr %v", err, syscall.EINVAL)
t.Errorf("NewCheckedArgs: error = %v, wantErr %v",
err, syscall.EINVAL)
}
t.Run("must panic", func(t *testing.T) {
t.Parallel()
badPayload := []string{"\x00"}
defer func() {
wantPanic := "invalid argument"
if r := recover(); r != wantPanic {
t.Errorf("MustNewCheckedArgs: panic = %v, wantPanic %v", r, wantPanic)
t.Errorf("MustNewCheckedArgs: panic = %v, wantPanic %v",
r, wantPanic)
}
}()
helper.MustNewCheckedArgs(badPayload)

View File

@ -12,13 +12,12 @@ import (
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/helper/proc"
"hakurei.app/message"
)
// New initialises a Helper instance with wt as the null-terminated argument writer.
func New(
ctx context.Context,
msg message.Msg,
msg container.Msg,
pathname *check.Absolute, name string,
wt io.WriterTo,
stat bool,

View File

@ -68,35 +68,39 @@ func genericStub(argsFile, statFile *os.File) {
}
}
// simulate status pipe behaviour
if statFile != nil {
// simulate status pipe behaviour
var epoll int
if fd, err := syscall.EpollCreate1(0); err != nil {
panic("cannot open epoll fd: " + err.Error())
} else {
defer func() {
if err = syscall.Close(fd); err != nil {
panic("cannot close epoll fd: " + err.Error())
}
}()
epoll = fd
}
if err := syscall.EpollCtl(epoll, syscall.EPOLL_CTL_ADD, int(statFile.Fd()), &syscall.EpollEvent{}); err != nil {
panic("cannot add status pipe to epoll: " + err.Error())
}
if _, err := statFile.Write([]byte{'x'}); err != nil {
panic("cannot write to status pipe: " + err.Error())
}
// wait for status pipe close
events := make([]syscall.EpollEvent, 1)
if _, err := syscall.EpollWait(epoll, events, -1); err != nil {
panic("cannot poll status pipe: " + err.Error())
}
if events[0].Events != syscall.EPOLLERR {
panic(strconv.Itoa(int(events[0].Events)))
done := make(chan struct{})
go func() {
// wait for status pipe close
var epoll int
if fd, err := syscall.EpollCreate1(0); err != nil {
panic("cannot open epoll fd: " + err.Error())
} else {
defer func() {
if err = syscall.Close(fd); err != nil {
panic("cannot close epoll fd: " + err.Error())
}
}()
epoll = fd
}
if err := syscall.EpollCtl(epoll, syscall.EPOLL_CTL_ADD, int(statFile.Fd()), &syscall.EpollEvent{}); err != nil {
panic("cannot add status pipe to epoll: " + err.Error())
}
events := make([]syscall.EpollEvent, 1)
if _, err := syscall.EpollWait(epoll, events, -1); err != nil {
panic("cannot poll status pipe: " + err.Error())
}
if events[0].Events != syscall.EPOLLERR {
panic(strconv.Itoa(int(events[0].Events)))
}
}
close(done)
}()
<-done
}
}

View File

@ -8,4 +8,8 @@ import (
"hakurei.app/helper"
)
func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) }
func TestMain(m *testing.M) {
container.TryArgv0(nil)
helper.InternalHelperStub()
os.Exit(m.Run())
}

View File

@ -8,11 +8,9 @@ import (
"hakurei.app/container/check"
)
// PrivateTmp is a private writable path in a hakurei container.
const PrivateTmp = "/.hakurei"
const Tmp = "/.hakurei"
// AbsPrivateTmp is a [check.Absolute] representation of [PrivateTmp].
var AbsPrivateTmp = check.MustAbs(PrivateTmp)
var AbsTmp = check.MustAbs(Tmp)
const (
// WaitDelayDefault is used when WaitDelay has its zero value.
@ -59,18 +57,18 @@ type (
// Init user namespace supplementary groups inherited by all container processes.
Groups []string `json:"groups"`
// High level configuration applied to the underlying [container].
// High level configuration applied to the underlying [container.Params].
Container *ContainerConfig `json:"container"`
}
// ContainerConfig describes the container configuration to be applied to an underlying [container].
// ContainerConfig describes the container configuration to be applied to an underlying [container.Params].
ContainerConfig struct {
// Container UTS namespace hostname.
Hostname string `json:"hostname,omitempty"`
// Duration in nanoseconds to wait for after interrupting the initial process.
// Defaults to [WaitDelayDefault] if zero, or [WaitDelayMax] if greater than [WaitDelayMax].
// Values lesser than zero is equivalent to zero, bypassing [WaitDelayDefault].
// Defaults to [WaitDelayDefault] if less than or equals to zero,
// or [WaitDelayMax] if greater than [WaitDelayMax].
WaitDelay time.Duration `json:"wait_delay,omitempty"`
// Emit Flatpak-compatible seccomp filter programs.

View File

@ -9,8 +9,6 @@ import (
)
func TestConfigValidate(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
config *hst.Config
@ -47,7 +45,6 @@ func TestConfigValidate(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if err := tc.config.Validate(); !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr)
}
@ -56,8 +53,6 @@ func TestConfigValidate(t *testing.T) {
}
func TestExtraPermConfig(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
config *hst.ExtraPermConfig
@ -67,8 +62,8 @@ func TestExtraPermConfig(t *testing.T) {
{"nil path", &hst.ExtraPermConfig{Path: nil}, "<invalid>"},
{"r", &hst.ExtraPermConfig{Path: fhs.AbsRoot, Read: true}, "r--:/"},
{"r+", &hst.ExtraPermConfig{Ensure: true, Path: fhs.AbsRoot, Read: true}, "r--+:/"},
{"w", &hst.ExtraPermConfig{Path: hst.AbsPrivateTmp, Write: true}, "-w-:/.hakurei"},
{"w+", &hst.ExtraPermConfig{Ensure: true, Path: hst.AbsPrivateTmp, Write: true}, "-w-+:/.hakurei"},
{"w", &hst.ExtraPermConfig{Path: hst.AbsTmp, Write: true}, "-w-:/.hakurei"},
{"w+", &hst.ExtraPermConfig{Ensure: true, Path: hst.AbsTmp, Write: true}, "-w-+:/.hakurei"},
{"x", &hst.ExtraPermConfig{Path: fhs.AbsRunUser, Execute: true}, "--x:/run/user/"},
{"x+", &hst.ExtraPermConfig{Ensure: true, Path: fhs.AbsRunUser, Execute: true}, "--x+:/run/user/"},
{"rwx", &hst.ExtraPermConfig{Path: fhs.AbsTmp, Read: true, Write: true, Execute: true}, "rwx:/tmp/"},
@ -77,7 +72,6 @@ func TestExtraPermConfig(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.config.String(); got != tc.want {
t.Errorf("String: %q, want %q", got, tc.want)
}

View File

@ -5,13 +5,11 @@ import (
"slices"
"testing"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/message"
)
func TestBadInterfaceError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
@ -25,22 +23,19 @@ func TestBadInterfaceError(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if gotError := tc.err.Error(); gotError != tc.want {
t.Errorf("Error: %s, want %s", gotError, tc.want)
}
if gotMessage, ok := message.GetMessage(tc.err); !ok {
t.Error("GetMessage: ok = false")
if gotMessage, ok := container.GetErrorMessage(tc.err); !ok {
t.Error("GetErrorMessage: ok = false")
} else if gotMessage != tc.want {
t.Errorf("GetMessage: %s, want %s", gotMessage, tc.want)
t.Errorf("GetErrorMessage: %s, want %s", gotMessage, tc.want)
}
})
}
}
func TestBusConfigInterfaces(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
c *hst.BusConfig
@ -68,7 +63,6 @@ func TestBusConfigInterfaces(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var got []string
if tc.cutoff > 0 {
var i int
@ -92,8 +86,6 @@ func TestBusConfigInterfaces(t *testing.T) {
}
func TestBusConfigCheckInterfaces(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
c *hst.BusConfig
@ -109,7 +101,6 @@ func TestBusConfigCheckInterfaces(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if err := tc.c.CheckInterfaces(tc.name); !reflect.DeepEqual(err, tc.err) {
t.Errorf("CheckInterfaces: error = %#v, want %#v", err, tc.err)
}

View File

@ -11,20 +11,14 @@ import (
type Enablement byte
const (
// EWayland exposes a wayland pathname socket via security-context-v1.
EWayland Enablement = 1 << iota
// EX11 adds the target user via X11 ChangeHosts and exposes the X11 pathname socket.
EX11
// EDBus enables the per-container xdg-dbus-proxy daemon.
EDBus
// EPulse copies the PulseAudio cookie to [hst.PrivateTmp] and exposes the PulseAudio socket.
EPulse
// EM is a noop.
EM
)
// String returns a string representation of the flags set on [Enablement].
func (e Enablement) String() string {
switch e {
case 0:
@ -57,17 +51,17 @@ func (e Enablement) String() string {
// NewEnablements returns the address of [Enablement] as [Enablements].
func NewEnablements(e Enablement) *Enablements { return (*Enablements)(&e) }
// Enablements is the [json] adapter for [Enablement].
type Enablements Enablement
// enablementsJSON is the [json] representation of [Enablements].
type enablementsJSON = struct {
// enablementsJSON is the [json] representation of the [Enablement] bit field.
type enablementsJSON struct {
Wayland bool `json:"wayland,omitempty"`
X11 bool `json:"x11,omitempty"`
DBus bool `json:"dbus,omitempty"`
Pulse bool `json:"pulse,omitempty"`
}
// Enablements is the [json] adapter for [Enablement].
type Enablements Enablement
// Unwrap returns the underlying [Enablement].
func (e *Enablements) Unwrap() Enablement {
if e == nil {

View File

@ -10,8 +10,6 @@ import (
)
func TestEnablementString(t *testing.T) {
t.Parallel()
testCases := []struct {
flags hst.Enablement
want string
@ -40,7 +38,6 @@ func TestEnablementString(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
t.Parallel()
if got := tc.flags.String(); got != tc.want {
t.Errorf("String: %q, want %q", got, tc.want)
}
@ -49,8 +46,6 @@ func TestEnablementString(t *testing.T) {
}
func TestEnablements(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
e *hst.Enablements
@ -68,10 +63,7 @@ func TestEnablements(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("marshal", func(t *testing.T) {
t.Parallel()
if got, err := json.Marshal(tc.e); err != nil {
t.Fatalf("Marshal: error = %v", err)
} else if string(got) != tc.data {
@ -89,8 +81,6 @@ func TestEnablements(t *testing.T) {
})
t.Run("unmarshal", func(t *testing.T) {
t.Parallel()
{
got := new(hst.Enablements)
if err := json.Unmarshal([]byte(tc.data), &got); err != nil {
@ -126,8 +116,6 @@ func TestEnablements(t *testing.T) {
}
t.Run("unwrap", func(t *testing.T) {
t.Parallel()
t.Run("nil", func(t *testing.T) {
if got := (*hst.Enablements)(nil).Unwrap(); got != 0 {
t.Errorf("Unwrap: %v", got)
@ -142,8 +130,6 @@ func TestEnablements(t *testing.T) {
})
t.Run("passthrough", func(t *testing.T) {
t.Parallel()
if _, err := (*hst.Enablements)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("MarshalJSON: error = %v", err)
}

View File

@ -47,16 +47,17 @@ type Ops interface {
Etc(host *check.Absolute, prefix string) Ops
}
// ApplyState holds the address of [Ops] and any relevant application state.
// ApplyState holds the address of [container.Ops] and any relevant application state.
type ApplyState struct {
// AutoEtcPrefix is the prefix for [FSBind] in autoetc [FSBind.Special] condition.
// AutoEtcPrefix is the prefix for [container.AutoEtcOp].
AutoEtcPrefix string
Ops
}
// ErrFSNull is returned by [json] on encountering a null [FilesystemConfig] value.
var ErrFSNull = errors.New("unexpected null in mount point")
var (
ErrFSNull = errors.New("unexpected null in mount point")
)
// FSTypeError is returned when [ContainerConfig.Filesystem] contains an entry with invalid type.
type FSTypeError string
@ -89,7 +90,7 @@ func (f *FilesystemConfigJSON) Valid() bool {
return f != nil && f.FilesystemConfig != nil && f.FilesystemConfig.Valid()
}
// fsType holds the string representation of the concrete type of [FilesystemConfig].
// fsType holds the string representation of a [FilesystemConfig]'s concrete type.
type fsType struct {
Type string `json:"type"`
}

View File

@ -15,8 +15,6 @@ import (
)
func TestFilesystemConfigJSON(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
want hst.FilesystemConfigJSON
@ -88,10 +86,7 @@ func TestFilesystemConfigJSON(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("marshal", func(t *testing.T) {
t.Parallel()
wantErr := tc.wantErr
if errors.As(wantErr, new(hst.FSTypeError)) {
// for unsupported implementation tc
@ -127,7 +122,6 @@ func TestFilesystemConfigJSON(t *testing.T) {
})
t.Run("unmarshal", func(t *testing.T) {
t.Parallel()
if tc.data == "\x00" && tc.sData == "\x00" {
if errors.As(tc.wantErr, new(hst.FSImplError)) {
// this error is only returned on marshal
@ -169,8 +163,6 @@ func TestFilesystemConfigJSON(t *testing.T) {
}
t.Run("valid", func(t *testing.T) {
t.Parallel()
if got := (*hst.FilesystemConfigJSON).Valid(nil); got {
t.Errorf("Valid: %v, want false", got)
}
@ -185,7 +177,6 @@ func TestFilesystemConfigJSON(t *testing.T) {
})
t.Run("passthrough", func(t *testing.T) {
t.Parallel()
if err := new(hst.FilesystemConfigJSON).UnmarshalJSON(make([]byte, 0)); err == nil {
t.Errorf("UnmarshalJSON: error = %v", err)
}
@ -193,10 +184,7 @@ func TestFilesystemConfigJSON(t *testing.T) {
}
func TestFSErrors(t *testing.T) {
t.Parallel()
t.Run("type", func(t *testing.T) {
t.Parallel()
want := `invalid filesystem type "cat"`
if got := hst.FSTypeError("cat").Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
@ -204,8 +192,6 @@ func TestFSErrors(t *testing.T) {
})
t.Run("impl", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
val hst.FilesystemConfig
@ -219,7 +205,6 @@ func TestFSErrors(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := hst.FSImplError{Value: tc.val}
if got := err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want)
@ -257,17 +242,13 @@ type fsTestCase struct {
func checkFs(t *testing.T, testCases []fsTestCase) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("valid", func(t *testing.T) {
t.Parallel()
if got := tc.fs.Valid(); got != tc.valid {
t.Errorf("Valid: %v, want %v", got, tc.valid)
}
})
t.Run("ops", func(t *testing.T) {
t.Parallel()
ops := new(container.Ops)
tc.fs.Apply(&hst.ApplyState{AutoEtcPrefix: ":3", Ops: opsAdapter{ops}})
if !reflect.DeepEqual(ops, &tc.ops) {
@ -284,21 +265,18 @@ func checkFs(t *testing.T, testCases []fsTestCase) {
})
t.Run("path", func(t *testing.T) {
t.Parallel()
if got := tc.fs.Path(); !reflect.DeepEqual(got, tc.path) {
t.Errorf("Target: %q, want %q", got, tc.path)
}
})
t.Run("host", func(t *testing.T) {
t.Parallel()
if got := tc.fs.Host(); !reflect.DeepEqual(got, tc.host) {
t.Errorf("Host: %q, want %q", got, tc.host)
}
})
t.Run("string", func(t *testing.T) {
t.Parallel()
if tc.str == "\x00" {
return
}

View File

@ -16,23 +16,22 @@ const FilesystemBind = "bind"
// FSBind represents a host to container bind mount.
type FSBind struct {
// Pathname in the container mount namespace. Same as Source if nil.
// mount point in container, same as Source if empty
Target *check.Absolute `json:"dst,omitempty"`
// Pathname in the init mount namespace. Must not be nil.
// host filesystem path to make available to the container
Source *check.Absolute `json:"src"`
// Do not remount Target read-only.
// This has no effect if Source is mounted read-only in the init mount namespace.
// do not mount Target read-only
Write bool `json:"write,omitempty"`
// Allow access to devices (special files) on Target, implies Write.
// do not disable device files on Target, implies Write
Device bool `json:"dev,omitempty"`
// Create Source as a directory in the init mount namespace if it does not exist.
// create Source as a directory if it does not exist
Ensure bool `json:"ensure,omitempty"`
// Silently skip this mount point if Source does not exist in the init mount namespace.
// skip this mount point if Source does not exist
Optional bool `json:"optional,omitempty"`
/* Enable special behaviour:
For autoroot: Target must be [fhs.Root].
For autoetc: Target must be [fhs.Etc]. */
// enable special behaviour:
// for autoroot, Target must be set to [fhs.AbsRoot];
// for autoetc, Target must be set to [fhs.AbsEtc]
Special bool `json:"special,omitempty"`
}

View File

@ -9,8 +9,6 @@ import (
)
func TestFSBind(t *testing.T) {
t.Parallel()
checkFs(t, []fsTestCase{
{"nil", (*hst.FSBind)(nil), false, nil, nil, nil, "<invalid>"},
{"ensure optional", &hst.FSBind{Source: m("/"), Ensure: true, Optional: true},

View File

@ -15,13 +15,13 @@ const FilesystemEphemeral = "ephemeral"
// FSEphemeral represents an ephemeral container mount point.
type FSEphemeral struct {
// Pathname in the container mount namespace.
Target *check.Absolute `json:"dst"`
// Do not mount filesystem read-only.
// mount point in container
Target *check.Absolute `json:"dst,omitempty"`
// do not mount filesystem read-only
Write bool `json:"write,omitempty"`
// Upper limit on the size of the filesystem.
// upper limit on the size of the filesystem
Size int `json:"size,omitempty"`
// Initial permission bits of the new filesystem.
// initial permission bits of the new filesystem
Perm os.FileMode `json:"perm,omitempty"`
}

View File

@ -9,8 +9,6 @@ import (
)
func TestFSEphemeral(t *testing.T) {
t.Parallel()
checkFs(t, []fsTestCase{
{"nil", (*hst.FSEphemeral)(nil), false, nil, nil, nil, "<invalid>"},
@ -38,15 +36,15 @@ func TestFSEphemeral(t *testing.T) {
"+ephemeral(-rwxr-xr-x):/run/nscd"},
{"negative size", &hst.FSEphemeral{
Target: hst.AbsPrivateTmp,
Target: hst.AbsTmp,
Write: true,
Size: -1,
}, true, container.Ops{&container.MountTmpfsOp{
FSName: "ephemeral",
Path: hst.AbsPrivateTmp,
Path: hst.AbsTmp,
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Perm: 0755,
}}, hst.AbsPrivateTmp, nil,
}}, hst.AbsTmp, nil,
"w+ephemeral(-rwxr-xr-x):/.hakurei"},
})
}

View File

@ -14,11 +14,11 @@ const FilesystemLink = "link"
// FSLink represents a symlink in the container filesystem.
type FSLink struct {
// Pathname in the container mount namespace.
// link path in container
Target *check.Absolute `json:"dst"`
// Arbitrary linkname value store in the symlink.
// linkname the symlink points to
Linkname string `json:"linkname"`
// Whether to treat Linkname as an absolute pathname and dereference before creating the link.
// whether to dereference linkname before creating the link
Dereference bool `json:"dereference,omitempty"`
}

View File

@ -8,8 +8,6 @@ import (
)
func TestFSLink(t *testing.T) {
t.Parallel()
checkFs(t, []fsTestCase{
{"nil", (*hst.FSLink)(nil), false, nil, nil, nil, "<invalid>"},
{"zero", new(hst.FSLink), false, nil, nil, nil, "<invalid>"},

View File

@ -14,14 +14,14 @@ const FilesystemOverlay = "overlay"
// FSOverlay represents an overlay mount point.
type FSOverlay struct {
// Pathname in the container mount namespace.
// mount point in container
Target *check.Absolute `json:"dst"`
// Any filesystem, does not need to be on a writable filesystem, must not be nil.
// any filesystem, does not need to be on a writable filesystem, must not be nil
Lower []*check.Absolute `json:"lower"`
// The upperdir is normally on a writable filesystem, leave as nil to mount Lower readonly.
// the upperdir is normally on a writable filesystem, leave as nil to mount Lower readonly
Upper *check.Absolute `json:"upper,omitempty"`
// The workdir needs to be an empty directory on the same filesystem as Upper, must not be nil if Upper is populated.
// the workdir needs to be an empty directory on the same filesystem as Upper, must not be nil if Upper is populated
Work *check.Absolute `json:"work,omitempty"`
}

View File

@ -9,8 +9,6 @@ import (
)
func TestFSOverlay(t *testing.T) {
t.Parallel()
checkFs(t, []fsTestCase{
{"nil", (*hst.FSOverlay)(nil), false, nil, nil, nil, "<invalid>"},
{"nil lower", &hst.FSOverlay{Target: m("/etc"), Lower: []*check.Absolute{nil}}, false, nil, nil, nil, "<invalid>"},

View File

@ -12,12 +12,9 @@ import (
// An AppError is returned while starting an app according to [hst.Config].
type AppError struct {
// A user-facing description of where the error occurred.
Step string `json:"step"`
// The underlying error value.
Err error
// An arbitrary error message, overriding the return value of Message if not empty.
Msg string `json:"message,omitempty"`
Step string
Err error
Msg string
}
func (e *AppError) Error() string { return e.Err.Error() }
@ -41,13 +38,13 @@ func (e *AppError) Message() string {
// Paths contains environment-dependent paths used by hakurei.
type Paths struct {
// Temporary directory returned by [os.TempDir], usually equivalent to [fhs.AbsTmp].
// temporary directory returned by [os.TempDir] (usually `/tmp`)
TempDir *check.Absolute `json:"temp_dir"`
// Shared directory specific to the hsu userid, usually (`/tmp/hakurei.%d`, [Info.User]).
// path to shared directory (usually `/tmp/hakurei.%d`, [Info.User])
SharePath *check.Absolute `json:"share_path"`
// Checked XDG_RUNTIME_DIR value, usually (`/run/user/%d`, uid).
// XDG_RUNTIME_DIR value (usually `/run/user/%d`, uid)
RuntimePath *check.Absolute `json:"runtime_path"`
// Shared directory specific to the hsu userid located in RuntimePath, usually (`/run/user/%d/hakurei`, uid).
// application runtime directory (usually `/run/user/%d/hakurei`)
RunDirPath *check.Absolute `json:"run_dir_path"`
}
@ -120,10 +117,11 @@ func Template() *Config {
{&FSEphemeral{Target: fhs.AbsTmp, Write: true, Perm: 0755}},
{&FSOverlay{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{fhs.AbsVarLib.Append("hakurei/base/org.nixos/ro-store")},
Upper: fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"),
Work: fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/work"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}},
{&FSBind{Source: check.MustAbs("/nix/store")}},
{&FSLink{Target: fhs.AbsRun.Append("current-system"), Linkname: "/run/current-system", Dereference: true}},
{&FSLink{Target: fhs.AbsRun.Append("opengl-driver"), Linkname: "/run/opengl-driver", Dereference: true}},
{&FSBind{Source: fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"),

View File

@ -8,14 +8,12 @@ import (
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/message"
)
func TestAppError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
@ -60,31 +58,26 @@ func TestAppError(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("error", func(t *testing.T) {
t.Parallel()
if got := tc.err.Error(); got != tc.s {
t.Errorf("Error: %s, want %s", got, tc.s)
}
})
t.Run("message", func(t *testing.T) {
t.Parallel()
gotMessage, gotMessageOk := message.GetMessage(tc.err)
gotMessage, gotMessageOk := container.GetErrorMessage(tc.err)
if want := tc.message != "\x00"; gotMessageOk != want {
t.Errorf("GetMessage: ok = %v, want %v", gotMessage, want)
t.Errorf("GetErrorMessage: ok = %v, want %v", gotMessage, want)
}
if gotMessageOk {
if gotMessage != tc.message {
t.Errorf("GetMessage: %s, want %s", gotMessage, tc.message)
t.Errorf("GetErrorMessage: %s, want %s", gotMessage, tc.message)
}
}
})
t.Run("is", func(t *testing.T) {
t.Parallel()
if !errors.Is(tc.err, tc.is) {
t.Errorf("Is: unexpected false for %v", tc.is)
}
@ -97,8 +90,6 @@ func TestAppError(t *testing.T) {
}
func TestTemplate(t *testing.T) {
t.Parallel()
const want = `{
"id": "org.chromium.Chromium",
"enablements": {
@ -202,10 +193,14 @@ func TestTemplate(t *testing.T) {
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/var/lib/hakurei/base/org.nixos/ro-store"
"/mnt-root/nix/.ro-store"
],
"upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
},
{
"type": "link",

View File

@ -6,13 +6,13 @@ import (
"log"
"os"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/message"
)
// Main runs an app according to [hst.Config] and terminates. Main does not return.
func Main(ctx context.Context, msg message.Msg, config *hst.Config) {
func Main(ctx context.Context, msg container.Msg, config *hst.Config) {
var id state.ID
if err := state.NewAppID(&id); err != nil {
log.Fatal(err)

View File

@ -9,6 +9,7 @@ import (
"io"
"io/fs"
"log"
"maps"
"os/exec"
"os/user"
"reflect"
@ -22,14 +23,12 @@ import (
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/message"
"hakurei.app/system"
"hakurei.app/system/acl"
)
func TestApp(t *testing.T) {
t.Parallel()
msg := message.NewMsg(nil)
msg := container.NewMsg(nil)
msg.SwapVerbose(testing.Verbose())
testCases := []struct {
@ -99,7 +98,7 @@ func TestApp(t *testing.T) {
Ops: new(container.Ops).
Root(m("/"), bits.BindWritable).
Proc(m("/proc/")).
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777).
Bind(m("/dev/kvm"), m("/dev/kvm"), bits.BindWritable|bits.BindDevice|bits.BindOptional).
@ -251,9 +250,9 @@ func TestApp(t *testing.T) {
Args: []string{"zsh", "-c", "exec chromium "},
Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
"HOME=/home/chronos",
"PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie",
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
@ -266,7 +265,7 @@ func TestApp(t *testing.T) {
Ops: new(container.Ops).
Root(m("/"), bits.BindWritable).
Proc(m("/proc/")).
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777).
Bind(m("/dev/dri"), m("/dev/dri"), bits.BindWritable|bits.BindDevice|bits.BindOptional).
@ -283,9 +282,9 @@ func TestApp(t *testing.T) {
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0).
Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0).
Place(m(hst.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
Place(m(hst.Tmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0).
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: bits.PresetExt | bits.PresetDenyDevel,
HostNet: true,
@ -395,9 +394,9 @@ func TestApp(t *testing.T) {
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
"HOME=/var/lib/persist/module/hakurei/0/1",
"PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie",
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
@ -409,7 +408,7 @@ func TestApp(t *testing.T) {
},
Ops: new(container.Ops).
Proc(m("/proc/")).
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777).
Bind(m("/bin"), m("/bin"), 0).
@ -433,9 +432,9 @@ func TestApp(t *testing.T) {
Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0).
Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0).
Place(m(hst.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
Place(m(hst.Tmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: bits.PresetExt | bits.PresetDenyTTY | bits.PresetDenyDevel,
HostNet: true,
@ -446,19 +445,29 @@ func TestApp(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
gr, gw := io.Pipe()
var gotSys *system.I
{
sPriv := newOutcomeState(tc.k, msg, &tc.id, tc.config, &Hsu{k: tc.k})
sPriv := outcomeState{
ID: &tc.id,
Identity: tc.config.Identity,
UserID: (&Hsu{k: tc.k}).MustIDMsg(msg),
EnvPaths: copyPaths(tc.k),
Container: tc.config.Container,
}
sPriv.populateEarly(tc.k, msg, tc.config)
if err := sPriv.populateLocal(tc.k, msg); err != nil {
t.Fatalf("populateLocal: error = %#v", err)
}
gotSys = system.New(t.Context(), msg, sPriv.uid.unwrap())
if err := sPriv.newSys(tc.config, gotSys).toSystem(); err != nil {
t.Fatalf("toSystem: error = %#v", err)
stateSys := outcomeStateSys{sys: gotSys, outcomeState: &sPriv}
for _, op := range sPriv.Shim.Ops {
if err := op.toSystem(&stateSys, tc.config); err != nil {
t.Fatalf("toSystem: error = %#v", err)
}
}
go func() {
@ -470,7 +479,7 @@ func TestApp(t *testing.T) {
}()
}
var gotParams *container.Params
var gotParams container.Params
{
var sShim outcomeState
@ -482,13 +491,17 @@ func TestApp(t *testing.T) {
t.Fatalf("populateLocal: error = %#v", err)
}
stateParams := sShim.newParams()
stateParams := outcomeStateParams{params: &gotParams, outcomeState: &sShim}
if sShim.Container.Env == nil {
stateParams.env = make(map[string]string, envAllocSize)
} else {
stateParams.env = maps.Clone(sShim.Container.Env)
}
for _, op := range sShim.Shim.Ops {
if err := op.toContainer(stateParams); err != nil {
if err := op.toContainer(&stateParams); err != nil {
t.Fatalf("toContainer: error = %#v", err)
}
}
gotParams = stateParams.params
}
t.Run("sys", func(t *testing.T) {
@ -498,8 +511,8 @@ func TestApp(t *testing.T) {
})
t.Run("params", func(t *testing.T) {
if !reflect.DeepEqual(gotParams, tc.wantParams) {
t.Errorf("toContainer: params =\n%s\n, want\n%s", mustMarshal(gotParams), mustMarshal(tc.wantParams))
if !reflect.DeepEqual(&gotParams, tc.wantParams) {
t.Errorf("toContainer: params =\n%s\n, want\n%s", mustMarshal(&gotParams), mustMarshal(tc.wantParams))
}
})
})
@ -563,7 +576,6 @@ type stubNixOS struct {
func (k *stubNixOS) new(func(k syscallDispatcher)) { panic("not implemented") }
func (k *stubNixOS) getpid() int { return 0xdeadbeef }
func (k *stubNixOS) getuid() int { return 1971 }
func (k *stubNixOS) getgid() int { return 100 }
@ -583,8 +595,6 @@ func (k *stubNixOS) lookupEnv(key string) (string, bool) {
return "/run/user/1971", true
case "XDG_CONFIG_HOME":
return "/home/ophestra/xdg/config", true
case "DBUS_SYSTEM_BUS_ADDRESS":
return "", false
default:
panic(fmt.Sprintf("attempted to access unexpected environment variable %q", key))
}
@ -659,7 +669,7 @@ func (k *stubNixOS) evalSymlinks(path string) (string, error) {
return "/run/user/1971", nil
case "/tmp/hakurei.0":
return "/tmp/hakurei.0", nil
case "/var/run/dbus":
case "/run/dbus":
return "/run/dbus", nil
case "/dev/kvm":
return "/dev/kvm", nil
@ -734,8 +744,8 @@ func (k *stubNixOS) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
}
}
func (k *stubNixOS) overflowUid(message.Msg) int { return 65534 }
func (k *stubNixOS) overflowGid(message.Msg) int { return 65534 }
func (k *stubNixOS) overflowUid(container.Msg) int { return 65534 }
func (k *stubNixOS) overflowGid(container.Msg) int { return 65534 }
func (k *stubNixOS) mustHsuPath() *check.Absolute { return m("/proc/nonexistent/hsu") }

View File

@ -12,7 +12,6 @@ import (
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/internal"
"hakurei.app/message"
)
// osFile represents [os.File].
@ -29,8 +28,6 @@ type syscallDispatcher interface {
// just synchronising access is not enough, as this is for test instrumentation.
new(f func(k syscallDispatcher))
// getpid provides [os.Getpid].
getpid() int
// getuid provides [os.Getuid].
getuid() int
// getgid provides [os.Getgid].
@ -56,9 +53,9 @@ type syscallDispatcher interface {
cmdOutput(cmd *exec.Cmd) ([]byte, error)
// overflowUid provides [container.OverflowUid].
overflowUid(msg message.Msg) int
overflowUid(msg container.Msg) int
// overflowGid provides [container.OverflowGid].
overflowGid(msg message.Msg) int
overflowGid(msg container.Msg) int
// mustHsuPath provides [internal.MustHsuPath].
mustHsuPath() *check.Absolute
@ -72,7 +69,6 @@ type direct struct{}
func (k direct) new(f func(k syscallDispatcher)) { go f(k) }
func (direct) getpid() int { return os.Getpid() }
func (direct) getuid() int { return os.Getuid() }
func (direct) getgid() int { return os.Getgid() }
func (direct) lookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
@ -94,8 +90,8 @@ func (direct) lookupGroupId(name string) (gid string, err error) {
func (direct) cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() }
func (direct) overflowUid(msg message.Msg) int { return container.OverflowUid(msg) }
func (direct) overflowGid(msg message.Msg) int { return container.OverflowGid(msg) }
func (direct) overflowUid(msg container.Msg) int { return container.OverflowUid(msg) }
func (direct) overflowGid(msg container.Msg) int { return container.OverflowGid(msg) }
func (direct) mustHsuPath() *check.Absolute { return internal.MustHsuPath() }

View File

@ -1,312 +1,16 @@
package app
import (
"bytes"
"io/fs"
"log"
"os"
"os/exec"
"reflect"
"slices"
"testing"
"time"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/message"
"hakurei.app/system"
)
// call initialises a [stub.Call].
// This keeps composites analysis happy without making the test cases too bloated.
func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
return stub.Call{Name: name, Args: args, Ret: ret, Err: err}
}
// checkExpectUid is the uid value used by checkOpBehaviour to initialise [system.I].
const checkExpectUid = 0xcafebabe
// wantAutoEtcPrefix is the autoetc prefix corresponding to checkExpectInstanceId.
const wantAutoEtcPrefix = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
// checkExpectInstanceId is the [state.ID] value used by checkOpBehaviour to initialise outcomeState.
var checkExpectInstanceId = *(*state.ID)(bytes.Repeat([]byte{0xaa}, len(state.ID{})))
type opBehaviourTestCase struct {
name string
newOp func(isShim, clearUnexported bool) outcomeOp
newConfig func() *hst.Config
pStateSys func(state *outcomeStateSys)
toSystem []stub.Call
wantSys *system.I
extraCheckSys func(t *testing.T, state *outcomeStateSys)
wantErrSystem error
pStateContainer func(state *outcomeStateParams)
toContainer []stub.Call
wantParams *container.Params
extraCheckParams func(t *testing.T, state *outcomeStateParams)
wantErrContainer error
}
func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Helper()
wantNewState := []stub.Call{
// newOutcomeState
call("getpid", stub.ExpectArgs{}, 0xdead, nil),
call("isVerbose", stub.ExpectArgs{}, true, nil),
call("mustHsuPath", stub.ExpectArgs{}, m(container.Nonexistent), nil),
call("cmdOutput", stub.ExpectArgs{container.Nonexistent, os.Stderr, []string{}, "/"}, []byte("0"), nil),
call("tempdir", stub.ExpectArgs{}, container.Nonexistent+"/tmp", nil),
call("lookupEnv", stub.ExpectArgs{"XDG_RUNTIME_DIR"}, container.Nonexistent+"/xdg_runtime_dir", nil),
call("getuid", stub.ExpectArgs{}, 1000, nil),
call("getgid", stub.ExpectArgs{}, 100, nil),
// populateLocal
call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{
m(container.Nonexistent + "/tmp/hakurei.0"),
m(container.Nonexistent + "/xdg_runtime_dir/hakurei"),
}}, nil, nil),
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
wantCallsFull := slices.Concat(wantNewState, tc.toSystem, []stub.Call{{Name: stub.CallSeparator}})
if tc.wantErrSystem == nil {
wantCallsFull = append(wantCallsFull, slices.Concat(wantNewState, tc.toContainer)...)
}
wantConfig := tc.newConfig()
k := &kstub{panicDispatcher{}, stub.New(t,
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{panicDispatcher{}, s} },
stub.Expect{Calls: wantCallsFull},
)}
defer stub.HandleExit(t)
{
config := tc.newConfig()
s := newOutcomeState(k, k, &checkExpectInstanceId, config, &Hsu{k: k})
if err := s.populateLocal(k, k); err != nil {
t.Fatalf("populateLocal: error = %v", err)
}
stateSys := s.newSys(config, newI())
if tc.pStateSys != nil {
tc.pStateSys(stateSys)
}
op := tc.newOp(false, true)
if err := op.toSystem(stateSys); !reflect.DeepEqual(err, tc.wantErrSystem) {
t.Fatalf("toSystem: error = %#v, want %#v", err, tc.wantErrSystem)
}
k.Expects(stub.CallSeparator)
if !reflect.DeepEqual(config, wantConfig) {
t.Errorf("toSystem clobbered config: %#v, want %#v", config, wantConfig)
}
if tc.wantErrSystem != nil {
goto out
}
if !stateSys.sys.Equal(tc.wantSys) {
t.Errorf("toSystem: %#v, want %#v", stateSys.sys, tc.wantSys)
}
if tc.extraCheckSys != nil {
tc.extraCheckSys(t, stateSys)
}
if wantOpSys := tc.newOp(true, false); !reflect.DeepEqual(op, wantOpSys) {
t.Errorf("toSystem: op = %#v, want %#v", op, wantOpSys)
}
}
{
config := tc.newConfig()
s := newOutcomeState(k, k, &checkExpectInstanceId, config, &Hsu{k: k})
stateParams := s.newParams()
if err := s.populateLocal(k, k); err != nil {
t.Fatalf("populateLocal: error = %v", err)
}
if tc.pStateContainer != nil {
tc.pStateContainer(stateParams)
}
op := tc.newOp(true, true)
if err := op.toContainer(stateParams); !reflect.DeepEqual(err, tc.wantErrContainer) {
t.Fatalf("toContainer: error = %#v, want %#v", err, tc.wantErrContainer)
}
if tc.wantErrContainer != nil {
goto out
}
if !reflect.DeepEqual(stateParams.params, tc.wantParams) {
t.Errorf("toContainer:\n%s\nwant\n%s", mustMarshal(stateParams.params), mustMarshal(tc.wantParams))
}
if tc.extraCheckParams != nil {
tc.extraCheckParams(t, stateParams)
}
}
out:
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
count := k.Pos() - 1 // separator
if count-len(wantNewState) < len(tc.toSystem) {
t.Errorf("toSystem: %d calls, want %d", count-len(wantNewState), len(tc.toSystem))
} else {
t.Errorf("toContainer: %d calls, want %d", count-len(tc.toSystem)-2*len(wantNewState), len(tc.toContainer))
}
})
})
}
}
func newI() *system.I { return system.New(panicMsgContext{}, panicMsgContext{}, checkExpectUid) }
type kstub struct {
panicDispatcher
*stub.Stub[syscallDispatcher]
}
func (k *kstub) getpid() int { k.Helper(); return k.Expects("getpid").Ret.(int) }
func (k *kstub) getuid() int { k.Helper(); return k.Expects("getuid").Ret.(int) }
func (k *kstub) getgid() int { k.Helper(); return k.Expects("getgid").Ret.(int) }
func (k *kstub) lookupEnv(key string) (string, bool) {
k.Helper()
expect := k.Expects("lookupEnv")
if expect.Error(
stub.CheckArg(k.Stub, "key", key, 0)) != nil {
k.FailNow()
}
if expect.Ret == nil {
return "\x00", false
}
return expect.Ret.(string), true
}
func (k *kstub) readdir(name string) ([]os.DirEntry, error) {
k.Helper()
expect := k.Expects("readdir")
return expect.Ret.([]os.DirEntry), expect.Error(
stub.CheckArg(k.Stub, "name", name, 0))
}
func (k *kstub) tempdir() string { k.Helper(); return k.Expects("tempdir").Ret.(string) }
func (k *kstub) evalSymlinks(path string) (string, error) {
k.Helper()
expect := k.Expects("evalSymlinks")
return expect.Ret.(string), expect.Error(
stub.CheckArg(k.Stub, "path", path, 0))
}
func (k *kstub) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
k.Helper()
expect := k.Expects("cmdOutput")
return expect.Ret.([]byte), expect.Error(
stub.CheckArg(k.Stub, "cmd.Path", cmd.Path, 0),
stub.CheckArgReflect(k.Stub, "cmd.Stderr", cmd.Stderr, 1),
stub.CheckArgReflect(k.Stub, "cmd.Env", cmd.Env, 2),
stub.CheckArg(k.Stub, "cmd.Dir", cmd.Dir, 3))
}
func (k *kstub) mustHsuPath() *check.Absolute {
k.Helper()
return k.Expects("mustHsuPath").Ret.(*check.Absolute)
}
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
func (k *kstub) SwapVerbose(verbose bool) bool {
k.Helper()
expect := k.Expects("swapVerbose")
if expect.Error(
stub.CheckArg(k.Stub, "verbose", verbose, 0)) != nil {
k.FailNow()
}
return expect.Ret.(bool)
}
// ignoreValue marks a value to be ignored by the test suite.
type ignoreValue struct{}
func (k *kstub) Verbose(v ...any) {
k.Helper()
expect := k.Expects("verbose")
// translate ignores in v
if want, ok := expect.Args[0].([]any); ok && len(v) == len(want) {
for i, a := range want {
if _, ok = a.(ignoreValue); ok {
v[i] = ignoreValue{}
}
}
}
if expect.Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
k.FailNow()
}
}
func (k *kstub) Verbosef(format string, v ...any) {
k.Helper()
if k.Expects("verbosef").Error(
stub.CheckArg(k.Stub, "format", format, 0),
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
k.FailNow()
}
}
func (k *kstub) Suspend() bool { k.Helper(); return k.Expects("suspend").Ret.(bool) }
func (k *kstub) Resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
func (k *kstub) BeforeExit() { k.Helper(); k.Expects("beforeExit") }
// stubDir returns a slice of [os.DirEntry] with only their Name method implemented.
func stubDir(names ...string) []os.DirEntry {
d := make([]os.DirEntry, len(names))
for i, name := range names {
d[i] = nameDentry(name)
}
return d
}
// nameDentry implements the Name method on [os.DirEntry].
type nameDentry string
func (e nameDentry) Name() string { return string(e) }
func (nameDentry) IsDir() bool { panic("unreachable") }
func (nameDentry) Type() fs.FileMode { panic("unreachable") }
func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") }
// panicMsgContext implements [message.Msg] and [context.Context] with methods wrapping panic.
// This should be assigned to test cases to be checked against.
type panicMsgContext struct{}
func (panicMsgContext) GetLogger() *log.Logger { panic("unreachable") }
func (panicMsgContext) IsVerbose() bool { panic("unreachable") }
func (panicMsgContext) SwapVerbose(bool) bool { panic("unreachable") }
func (panicMsgContext) Verbose(...any) { panic("unreachable") }
func (panicMsgContext) Verbosef(string, ...any) { panic("unreachable") }
func (panicMsgContext) Suspend() bool { panic("unreachable") }
func (panicMsgContext) Resume() bool { panic("unreachable") }
func (panicMsgContext) BeforeExit() { panic("unreachable") }
func (panicMsgContext) Deadline() (time.Time, bool) { panic("unreachable") }
func (panicMsgContext) Done() <-chan struct{} { panic("unreachable") }
func (panicMsgContext) Err() error { panic("unreachable") }
func (panicMsgContext) Value(any) any { panic("unreachable") }
// panicDispatcher implements syscallDispatcher with methods wrapping panic.
// This type is meant to be embedded in partial syscallDispatcher implementations.
type panicDispatcher struct{}
func (panicDispatcher) new(func(k syscallDispatcher)) { panic("unreachable") }
func (panicDispatcher) getpid() int { panic("unreachable") }
func (panicDispatcher) getuid() int { panic("unreachable") }
func (panicDispatcher) getgid() int { panic("unreachable") }
func (panicDispatcher) lookupEnv(string) (string, bool) { panic("unreachable") }
@ -317,7 +21,7 @@ func (panicDispatcher) tempdir() string { panic("unreachab
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
func (panicDispatcher) overflowUid(container.Msg) int { panic("unreachable") }
func (panicDispatcher) overflowGid(container.Msg) int { panic("unreachable") }
func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") }
func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") }

View File

@ -13,8 +13,6 @@ import (
)
func TestEnvPaths(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
env *EnvPaths
@ -50,7 +48,6 @@ func TestEnvPaths(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if tc.wantPanic != "" {
defer func() {
if r := recover(); r != tc.wantPanic {
@ -69,8 +66,6 @@ func TestEnvPaths(t *testing.T) {
}
func TestCopyPaths(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
env map[string]string
@ -89,7 +84,6 @@ func TestCopyPaths(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if tc.fatal != "" {
defer stub.HandleExit(t)
}

View File

@ -1,16 +1,19 @@
package app
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"io"
"os"
"os/user"
"sync/atomic"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/message"
"hakurei.app/system"
)
@ -21,14 +24,16 @@ func newWithMessageError(msg string, err error) error {
// An outcome is the runnable state of a hakurei container via [hst.Config].
type outcome struct {
// initial [hst.Config] gob stream for state data;
// this is prepared ahead of time as config is clobbered during seal creation
ct io.WriterTo
// Supplementary group ids. Populated during finalise.
supp []string
// Resolved priv side operating system interactions. Populated during finalise.
sys *system.I
// Transmitted to shim. Populated during finalise.
state *outcomeState
// Kept for saving to [state].
config *hst.Config
// Whether the current process is in outcome.main.
active atomic.Bool
@ -37,7 +42,7 @@ type outcome struct {
syscallDispatcher
}
func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *state.ID, config *hst.Config) error {
func (k *outcome) finalise(ctx context.Context, msg container.Msg, id *state.ID, config *hst.Config) error {
if ctx == nil || id == nil {
// unreachable
panic("invalid call to finalise")
@ -52,6 +57,16 @@ func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *state.ID, c
return err
}
// TODO(ophestra): do not clobber during finalise
{
// encode initial configuration for state tracking
ct := new(bytes.Buffer)
if err := gob.NewEncoder(ct).Encode(config); err != nil {
return &hst.AppError{Step: "encode initial config", Err: err}
}
k.ct = ct
}
// hsu expects numerical group ids
supp := make([]string, len(config.Groups))
for i, name := range config.Groups {
@ -68,19 +83,28 @@ func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *state.ID, c
}
// early validation complete at this point
s := newOutcomeState(k.syscallDispatcher, msg, id, config, &Hsu{k: k})
s := outcomeState{
ID: id,
Identity: config.Identity,
UserID: (&Hsu{k: k}).MustIDMsg(msg),
EnvPaths: copyPaths(k.syscallDispatcher),
Container: config.Container,
}
s.populateEarly(k.syscallDispatcher, msg, config)
if err := s.populateLocal(k.syscallDispatcher, msg); err != nil {
return err
}
sys := system.New(k.ctx, msg, s.uid.unwrap())
if err := s.newSys(config, sys).toSystem(); err != nil {
return err
stateSys := outcomeStateSys{sys: sys, outcomeState: &s}
for _, op := range s.Shim.Ops {
if err := op.toSystem(&stateSys, config); err != nil {
return err
}
}
k.sys = sys
k.supp = supp
k.state = s
k.config = config
k.state = &s
return nil
}

View File

@ -9,9 +9,9 @@ import (
"strconv"
"sync"
"hakurei.app/container"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/message"
)
// Hsu caches responses from cmd/hsu.
@ -74,7 +74,7 @@ func (h *Hsu) ID() (int, error) {
func (h *Hsu) MustID() int { return h.MustIDMsg(nil) }
// MustIDMsg implements MustID with a custom [container.Msg].
func (h *Hsu) MustIDMsg(msg message.Msg) int {
func (h *Hsu) MustIDMsg(msg container.Msg) int {
id, err := h.ID()
if err == nil {
return id
@ -87,7 +87,7 @@ func (h *Hsu) MustIDMsg(msg message.Msg) int {
}
os.Exit(1)
return -0xdeadbeef
} else if m, ok := message.GetMessage(err); ok {
} else if m, ok := container.GetErrorMessage(err); ok {
log.Fatal(m)
return -0xdeadbeef
} else {

View File

@ -1,15 +1,13 @@
package app
import (
"errors"
"maps"
"os"
"strconv"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/message"
"hakurei.app/system"
"hakurei.app/system/acl"
)
@ -57,10 +55,13 @@ type outcomeState struct {
sc hst.Paths
*EnvPaths
// Matched paths to cover. Populated by spFilesystemOp.
HidePaths []*check.Absolute
// Copied via populateLocal.
k syscallDispatcher
// Copied via populateLocal.
msg message.Msg
msg container.Msg
}
// valid checks outcomeState to be safe for use with outcomeOp.
@ -72,21 +73,13 @@ func (s *outcomeState) valid() bool {
s.EnvPaths != nil
}
// newOutcomeState returns the address of a new outcomeState with its exported fields populated via syscallDispatcher.
func newOutcomeState(k syscallDispatcher, msg message.Msg, id *state.ID, config *hst.Config, hsu *Hsu) *outcomeState {
s := outcomeState{
Shim: &shimParams{PrivPID: k.getpid(), Verbose: msg.IsVerbose()},
ID: id,
Identity: config.Identity,
UserID: hsu.MustIDMsg(msg),
EnvPaths: copyPaths(k),
Container: config.Container,
}
// populateEarly populates exported fields via syscallDispatcher.
// This must only be called from the priv side.
func (s *outcomeState) populateEarly(k syscallDispatcher, msg container.Msg, config *hst.Config) {
s.Shim = &shimParams{PrivPID: os.Getpid(), Verbose: msg.IsVerbose(), Ops: fromConfig(config)}
// enforce bounds and default early
if s.Container.WaitDelay < 0 {
s.Shim.WaitDelay = 0
} else if s.Container.WaitDelay == 0 {
if s.Container.WaitDelay <= 0 {
s.Shim.WaitDelay = hst.WaitDelayDefault
} else if s.Container.WaitDelay > hst.WaitDelayMax {
s.Shim.WaitDelay = hst.WaitDelayMax
@ -100,12 +93,12 @@ func newOutcomeState(k syscallDispatcher, msg message.Msg, id *state.ID, config
s.Mapuid, s.Mapgid = k.overflowUid(msg), k.overflowGid(msg)
}
return &s
return
}
// populateLocal populates unexported fields from transmitted exported fields.
// These fields are cheaper to recompute per-process.
func (s *outcomeState) populateLocal(k syscallDispatcher, msg message.Msg) error {
func (s *outcomeState) populateLocal(k syscallDispatcher, msg container.Msg) error {
if !s.valid() || k == nil || msg == nil {
return newWithMessage("impossible outcome state reached")
}
@ -148,43 +141,10 @@ type outcomeStateSys struct {
// Process-specific directory in XDG_RUNTIME_DIR, nil if unused.
runtimeSharePath *check.Absolute
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
appId string
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
et hst.Enablement
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
directWayland bool
// Copied header from [hst.Config]. Safe for read by spFinalOp.toSystem only.
extraPerms []*hst.ExtraPermConfig
// Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only.
sessionBus, systemBus *hst.BusConfig
sys *system.I
*outcomeState
}
// newSys returns the address of a new outcomeStateSys embedding the current outcomeState.
func (s *outcomeState) newSys(config *hst.Config, sys *system.I) *outcomeStateSys {
return &outcomeStateSys{
appId: config.ID, et: config.Enablements.Unwrap(),
directWayland: config.DirectWayland, extraPerms: config.ExtraPerms,
sessionBus: config.SessionBus, systemBus: config.SystemBus,
sys: sys, outcomeState: s,
}
}
// newParams returns the address of a new outcomeStateParams embedding the current outcomeState.
func (s *outcomeState) newParams() *outcomeStateParams {
stateParams := outcomeStateParams{params: new(container.Params), outcomeState: s}
if s.Container.Env == nil {
stateParams.env = make(map[string]string, envAllocSize)
} else {
stateParams.env = maps.Clone(s.Container.Env)
}
return &stateParams
}
// ensureRuntimeDir must be called if access to paths within XDG_RUNTIME_DIR is required.
func (state *outcomeStateSys) ensureRuntimeDir() {
if state.useRuntimeDir {
@ -240,15 +200,12 @@ type outcomeStateParams struct {
*outcomeState
}
// errNotEnabled is returned by outcomeOp.toSystem and used internally to exclude an outcomeOp from transmission.
var errNotEnabled = errors.New("op not enabled in the configuration")
// An outcomeOp inflicts an outcome on [system.I] and contains enough information to
// inflict it on [container.Params] in a separate process.
// An implementation of outcomeOp must store cross-process states in exported fields only.
type outcomeOp interface {
// toSystem inflicts the current outcome on [system.I] in the priv side process.
toSystem(state *outcomeStateSys) error
toSystem(state *outcomeStateSys, config *hst.Config) error
// toContainer inflicts the current outcome on [container.Params] in the shim process.
// The implementation must not write to the Env field of [container.Params] as it will be overwritten
@ -256,45 +213,36 @@ type outcomeOp interface {
toContainer(state *outcomeStateParams) error
}
// toSystem calls the outcomeOp.toSystem method on all outcomeOp implementations and populates shimParams.Ops.
// fromConfig returns a corresponding slice of outcomeOp for [hst.Config].
// This function assumes the caller has already called the Validate method on [hst.Config]
// and checked that it returns nil.
func (state *outcomeStateSys) toSystem() error {
if state.Shim == nil || state.Shim.Ops != nil {
return newWithMessage("invalid ops state reached")
}
ops := [...]outcomeOp{
func fromConfig(config *hst.Config) (ops []outcomeOp) {
ops = []outcomeOp{
// must run first
&spParamsOp{},
// TODO(ophestra): move this late for #8 and #9
&spFilesystemOp{},
spFilesystemOp{},
spRuntimeOp{},
spTmpdirOp{},
spAccountOp{},
// optional via enablements
&spWaylandOp{},
&spX11Op{},
&spPulseOp{},
&spDBusOp{},
spFinalOp{},
}
state.Shim.Ops = make([]outcomeOp, 0, len(ops))
for _, op := range ops {
if err := op.toSystem(state); err != nil {
// this error is used internally to exclude this outcomeOp from transmission
if errors.Is(err, errNotEnabled) {
continue
}
return err
}
state.Shim.Ops = append(state.Shim.Ops, op)
et := config.Enablements.Unwrap()
if et&hst.EWayland != 0 {
ops = append(ops, &spWaylandOp{})
}
return nil
if et&hst.EX11 != 0 {
ops = append(ops, &spX11Op{})
}
if et&hst.EPulse != 0 {
ops = append(ops, &spPulseOp{})
}
if et&hst.EDBus != 0 {
ops = append(ops, &spDBusOp{})
}
ops = append(ops, spFinal{})
return
}

View File

@ -1,6 +1,7 @@
package app
import (
"reflect"
"testing"
"hakurei.app/hst"
@ -8,8 +9,6 @@ import (
)
func TestOutcomeStateValid(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
s *outcomeState
@ -25,10 +24,55 @@ func TestOutcomeStateValid(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.s.valid(); got != tc.want {
t.Errorf("valid: %v, want %v", got, tc.want)
}
})
}
}
func TestFromConfig(t *testing.T) {
testCases := []struct {
name string
config *hst.Config
want []outcomeOp
}{
{"ne", new(hst.Config), []outcomeOp{
&spParamsOp{},
spFilesystemOp{},
spRuntimeOp{},
spTmpdirOp{},
spAccountOp{},
spFinal{},
}},
{"wayland pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EWayland | hst.EPulse)}, []outcomeOp{
&spParamsOp{},
spFilesystemOp{},
spRuntimeOp{},
spTmpdirOp{},
spAccountOp{},
&spWaylandOp{},
&spPulseOp{},
spFinal{},
}},
{"all", &hst.Config{Enablements: hst.NewEnablements(0xff)}, []outcomeOp{
&spParamsOp{},
spFilesystemOp{},
spRuntimeOp{},
spTmpdirOp{},
spAccountOp{},
&spWaylandOp{},
&spX11Op{},
&spPulseOp{},
&spDBusOp{},
spFinal{},
}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := fromConfig(tc.config); !reflect.DeepEqual(got, tc.want) {
t.Errorf("fromConfig: %#v, want %#v", got, tc.want)
}
})
}
}

View File

@ -5,8 +5,6 @@ import (
)
func TestDeepContainsH(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
basepath string
@ -77,7 +75,6 @@ func TestDeepContainsH(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got, err := deepContainsH(tc.basepath, tc.targpath); (err != nil) != tc.wantErr {
t.Errorf("deepContainsH() error = %v, wantErr %v", err, tc.wantErr)
} else if got != tc.want {

View File

@ -17,7 +17,6 @@ import (
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/app/state"
"hakurei.app/message"
"hakurei.app/system"
)
@ -41,7 +40,7 @@ type mainState struct {
cmdWait chan error
k *outcome
message.Msg
container.Msg
uintptr
}
@ -208,7 +207,7 @@ func (ms mainState) fatal(fallback string, ferr error) {
}
// main carries out outcome and terminates. main does not return.
func (k *outcome) main(msg message.Msg) {
func (k *outcome) main(msg container.Msg) {
if !k.active.CompareAndSwap(false, true) {
panic("outcome: attempted to run twice")
}
@ -290,11 +289,10 @@ func (k *outcome) main(msg message.Msg) {
// shim accepted setup payload, create process state
if ok, err := ms.store.Do(k.state.identity.unwrap(), func(c state.Cursor) {
if err := c.Save(&state.State{
ID: k.state.id.unwrap(),
PID: ms.cmd.Process.Pid,
Config: k.config,
Time: *ms.Time,
}); err != nil {
ID: k.state.id.unwrap(),
PID: ms.cmd.Process.Pid,
Time: *ms.Time,
}, k.ct); err != nil {
ms.fatal("cannot save state entry:", err)
}
}); err != nil {
@ -313,10 +311,10 @@ func (k *outcome) main(msg message.Msg) {
os.Exit(0)
}
// printMessageError prints the error message according to [message.GetMessage],
// printMessageError prints the error message according to [container.GetErrorMessage],
// or fallback prepended to err if an error message is not available.
func printMessageError(fallback string, err error) {
m, ok := message.GetMessage(err)
m, ok := container.GetErrorMessage(err)
if !ok {
log.Println(fallback, err)
return

View File

@ -5,6 +5,7 @@ import (
"errors"
"io"
"log"
"maps"
"os"
"os/exec"
"os/signal"
@ -17,7 +18,6 @@ import (
"hakurei.app/container/bits"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/message"
)
//#include "shim-signal.h"
@ -47,13 +47,17 @@ type shimParams struct {
}
// valid checks shimParams to be safe for use.
func (p *shimParams) valid() bool { return p != nil && p.PrivPID > 0 }
func (p *shimParams) valid() bool {
return p != nil &&
p.Ops != nil &&
p.PrivPID > 0
}
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
func ShimMain() {
log.SetPrefix("shim: ")
log.SetFlags(0)
msg := message.NewMsg(log.Default())
msg := container.NewMsg(log.Default())
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
@ -77,7 +81,7 @@ func ShimMain() {
closeSetup = f
if err = state.populateLocal(direct{}, msg); err != nil {
if m, ok := message.GetMessage(err); ok {
if m, ok := container.GetErrorMessage(err); ok {
log.Fatal(m)
} else {
log.Fatalf("cannot populate local state: %v", err)
@ -101,10 +105,16 @@ func ShimMain() {
log.Fatalf("cannot set parent-death signal: %v", errno)
}
stateParams := state.newParams()
var params container.Params
stateParams := outcomeStateParams{params: &params, outcomeState: &state}
if state.Container.Env == nil {
stateParams.env = make(map[string]string, envAllocSize)
} else {
stateParams.env = maps.Clone(state.Container.Env)
}
for _, op := range state.Shim.Ops {
if err := op.toContainer(stateParams); err != nil {
if m, ok := message.GetMessage(err); ok {
if err := op.toContainer(&stateParams); err != nil {
if m, ok := container.GetErrorMessage(err); ok {
log.Fatal(m)
} else {
log.Fatalf("cannot create container state: %v", err)
@ -123,7 +133,7 @@ func ShimMain() {
switch buf[0] {
case 0: // got SIGCONT from monitor: shim exit requested
if fp := cancelContainer.Load(); stateParams.params.ForwardCancel && fp != nil && *fp != nil {
if fp := cancelContainer.Load(); params.ForwardCancel && fp != nil && *fp != nil {
(*fp)()
// shim now bound by ShimWaitDelay, implemented below
continue
@ -151,7 +161,7 @@ func ShimMain() {
}
}()
if stateParams.params.Ops == nil {
if params.Ops == nil {
log.Fatal("invalid container params")
}
@ -164,7 +174,7 @@ func ShimMain() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
cancelContainer.Store(&stop)
z := container.New(ctx, msg)
z.Params = *stateParams.params
z.Params = params
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
// bounds and default enforced in finalise.go

View File

@ -6,6 +6,7 @@ import (
"syscall"
"hakurei.app/container/fhs"
"hakurei.app/hst"
)
func init() { gob.Register(spAccountOp{}) }
@ -13,36 +14,31 @@ func init() { gob.Register(spAccountOp{}) }
// spAccountOp sets up user account emulation inside the container.
type spAccountOp struct{}
func (s spAccountOp) toSystem(state *outcomeStateSys) error {
func (s spAccountOp) toSystem(state *outcomeStateSys, _ *hst.Config) error {
const fallbackUsername = "chronos"
// do checks here to fail before fork/exec
if state.Container == nil || state.Container.Home == nil || state.Container.Shell == nil {
// unreachable
return syscall.ENOTRECOVERABLE
}
// default is applied in toContainer
if state.Container.Username != "" && !isValidUsername(state.Container.Username) {
if state.Container.Username == "" {
state.Container.Username = fallbackUsername
} else if !isValidUsername(state.Container.Username) {
return newWithMessage(fmt.Sprintf("invalid user name %q", state.Container.Username))
}
return nil
}
func (s spAccountOp) toContainer(state *outcomeStateParams) error {
const fallbackUsername = "chronos"
username := state.Container.Username
if username == "" {
username = fallbackUsername
}
state.params.Dir = state.Container.Home
state.env["HOME"] = state.Container.Home.String()
state.env["USER"] = username
state.env["USER"] = state.Container.Username
state.env["SHELL"] = state.Container.Shell.String()
state.params.
Place(fhs.AbsEtc.Append("passwd"),
[]byte(username+":x:"+
[]byte(state.Container.Username+":x:"+
state.mapuid.String()+":"+
state.mapgid.String()+
":Hakurei:"+

View File

@ -1,89 +0,0 @@
package app
import (
"maps"
"os"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/container/stub"
"hakurei.app/hst"
)
func TestSpAccountOp(t *testing.T) {
t.Parallel()
config := hst.Template()
checkOpBehaviour(t, []opBehaviourTestCase{
{"invalid state", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config {
c := hst.Template()
c.Container.Shell = nil
return c
}, nil, []stub.Call{
// this op performs basic validation and does not make calls during toSystem
}, nil, nil, syscall.ENOTRECOVERABLE, nil, nil, nil, nil, nil},
{"invalid user name", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config {
c := hst.Template()
c.Container.Username = "9"
return c
}, nil, []stub.Call{
// this op performs basic validation and does not make calls during toSystem
}, nil, nil, &hst.AppError{
Step: "finalise",
Err: os.ErrInvalid,
Msg: `invalid user name "9"`,
}, nil, nil, nil, nil, nil},
{"success fallback username", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config {
c := hst.Template()
c.Container.Username = ""
return c
}, nil, []stub.Call{
// this op performs basic validation and does not make calls during toSystem
}, newI(), nil, nil, func(state *outcomeStateParams) {
state.params.Ops = new(container.Ops)
}, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Dir: config.Container.Home,
Ops: new(container.Ops).
Place(m("/etc/passwd"), []byte("chronos:x:1000:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:100:\n")),
}, func(t *testing.T, state *outcomeStateParams) {
wantEnv := map[string]string{
"HOME": config.Container.Home.String(),
"USER": config.Container.Username,
"SHELL": config.Container.Shell.String(),
}
maps.Copy(wantEnv, config.Container.Env)
if !maps.Equal(state.env, wantEnv) {
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
}
}, nil},
{"success", func(bool, bool) outcomeOp { return spAccountOp{} }, hst.Template, nil, []stub.Call{
// this op performs basic validation and does not make calls during toSystem
}, newI(), nil, nil, func(state *outcomeStateParams) {
state.params.Ops = new(container.Ops)
}, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Dir: config.Container.Home,
Ops: new(container.Ops).
Place(m("/etc/passwd"), []byte("chronos:x:1000:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:100:\n")),
}, func(t *testing.T, state *outcomeStateParams) {
wantEnv := map[string]string{
"HOME": config.Container.Home.String(),
"USER": config.Container.Username,
"SHELL": config.Container.Shell.String(),
}
maps.Copy(wantEnv, config.Container.Env)
if !maps.Equal(state.env, wantEnv) {
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
}
}, nil},
})
}

View File

@ -15,7 +15,6 @@ import (
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/message"
"hakurei.app/system/dbus"
)
@ -32,7 +31,7 @@ type spParamsOp struct {
TermSet bool
}
func (s *spParamsOp) toSystem(state *outcomeStateSys) error {
func (s *spParamsOp) toSystem(state *outcomeStateSys, _ *hst.Config) error {
s.Term, s.TermSet = state.k.lookupEnv("TERM")
state.sys.Ensure(state.sc.SharePath, 0711)
return nil
@ -65,7 +64,7 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
// this behaviour is implemented in the shim
state.params.ForwardCancel = state.Shim.WaitDelay > 0
state.params.ForwardCancel = state.Container.WaitDelay >= 0
if state.Container.Multiarch {
state.params.SeccompFlags |= seccomp.AllowMultiarch
@ -105,7 +104,7 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
// early mount points
state.params.
Proc(fhs.AbsProc).
Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755)
Tmpfs(hst.AbsTmp, 1<<12, 0755)
if !state.Container.Device {
state.params.DevWritable(fhs.AbsDev, true)
} else {
@ -117,15 +116,12 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
return nil
}
func init() { gob.Register(new(spFilesystemOp)) }
func init() { gob.Register(spFilesystemOp{}) }
// spFilesystemOp applies configured filesystems to [container.Params], excluding the optional root filesystem.
type spFilesystemOp struct {
// Matched paths to cover. Stored during toSystem.
HidePaths []*check.Absolute
}
type spFilesystemOp struct{}
func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error {
func (s spFilesystemOp) toSystem(state *outcomeStateSys, _ *hst.Config) error {
/* retrieve paths and hide them if they're made available in the sandbox;
this feature tries to improve user experience of permissive defaults, and
@ -139,12 +135,7 @@ func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error {
varRunNscd,
}
// dbus.Address does not go through syscallDispatcher
systemBusAddr := dbus.FallbackSystemBusAddress
if addr, ok := state.k.lookupEnv(dbus.SystemBusAddress); ok {
systemBusAddr = addr
}
_, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
return &hst.AppError{Step: "parse dbus address", Err: err}
} else {
@ -252,9 +243,16 @@ func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error {
for i, ok := range hidePathMatch {
if ok {
if a, err := check.NewAbs(hidePaths[i]); err != nil {
return newWithMessage("invalid path hiding candidate " + strconv.Quote(hidePaths[i]))
var absoluteError *check.AbsoluteError
if !errors.As(err, &absoluteError) {
return newWithMessageError(absoluteError.Error(), absoluteError)
}
if absoluteError == nil {
return newWithMessage("impossible path checking state reached")
}
return newWithMessage("invalid path hiding candidate " + strconv.Quote(absoluteError.Pathname))
} else {
s.HidePaths = append(s.HidePaths, a)
state.HidePaths = append(state.HidePaths, a)
}
}
}
@ -262,7 +260,7 @@ func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error {
return nil
}
func (s *spFilesystemOp) toContainer(state *outcomeStateParams) error {
func (s spFilesystemOp) toContainer(state *outcomeStateParams) error {
for i, c := range state.filesystem {
if !c.Valid() {
return newWithMessage("invalid filesystem at index " + strconv.Itoa(i))
@ -270,7 +268,7 @@ func (s *spFilesystemOp) toContainer(state *outcomeStateParams) error {
c.Apply(&state.as)
}
for _, a := range s.HidePaths {
for _, a := range state.HidePaths {
state.params.Tmpfs(a, 1<<13, 0755)
}
@ -301,7 +299,7 @@ func resolveRoot(c *hst.ContainerConfig) (rootfs hst.FilesystemConfig, filesyste
}
// evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist].
func evalSymlinks(msg message.Msg, k syscallDispatcher, v *string) error {
func evalSymlinks(msg container.Msg, k syscallDispatcher, v *string) error {
if p, err := k.evalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err

View File

@ -1,427 +0,0 @@
package app
import (
"errors"
"maps"
"os"
"reflect"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/system/dbus"
)
func TestSpParamsOp(t *testing.T) {
t.Parallel()
config := hst.Template()
checkOpBehaviour(t, []opBehaviourTestCase{
{"invalid program path", func(isShim, _ bool) outcomeOp {
if !isShim {
return new(spParamsOp)
}
return &spParamsOp{Term: "xterm", TermSet: true}
}, func() *hst.Config {
c := hst.Template()
c.Container.Path = nil
return c
}, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
}, newI().
Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, nil, nil, &hst.AppError{
Step: "finalise",
Err: os.ErrInvalid,
Msg: "invalid program path",
}},
{"success defaultargs secure", func(isShim, _ bool) outcomeOp {
if !isShim {
return new(spParamsOp)
}
return &spParamsOp{Term: "xterm", TermSet: true}
}, func() *hst.Config {
c := hst.Template()
c.Container.Args = nil
c.Container.Multiarch = false
c.Container.SeccompCompat = false
c.Container.Devel = false
c.Container.Userns = false
c.Container.Tty = false
c.Container.Device = false
return c
}, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
}, newI().
Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Hostname: config.Container.Hostname,
HostNet: config.Container.HostNet,
HostAbstract: config.Container.HostAbstract,
Path: config.Container.Path,
Args: []string{config.Container.Path.String()},
SeccompPresets: bits.PresetExt | bits.PresetDenyDevel | bits.PresetDenyNS | bits.PresetDenyTTY,
Uid: 1000,
Gid: 100,
Ops: new(container.Ops).
Root(m("/var/lib/hakurei/base/org.debian"), bits.BindWritable).
Proc(fhs.AbsProc).Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
DevWritable(fhs.AbsDev, true).
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777),
}, func(t *testing.T, state *outcomeStateParams) {
wantEnv := map[string]string{
"TERM": "xterm",
}
maps.Copy(wantEnv, config.Container.Env)
if !maps.Equal(state.env, wantEnv) {
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
}
const wantAutoEtcPrefix = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
if state.as.AutoEtcPrefix != wantAutoEtcPrefix {
t.Errorf("toContainer: as.AutoEtcPrefix = %q, want %q", state.as.AutoEtcPrefix, wantAutoEtcPrefix)
}
wantFilesystems := config.Container.Filesystem[1:]
if !reflect.DeepEqual(state.filesystem, wantFilesystems) {
t.Errorf("toContainer: filesystem = %#v, want %#v", state.filesystem, wantFilesystems)
}
}, nil},
{"success", func(isShim, _ bool) outcomeOp {
if !isShim {
return new(spParamsOp)
}
return &spParamsOp{Term: "xterm", TermSet: true}
}, hst.Template, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
}, newI().
Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Hostname: config.Container.Hostname,
RetainSession: config.Container.Tty,
HostNet: config.Container.HostNet,
HostAbstract: config.Container.HostAbstract,
Path: config.Container.Path,
Args: config.Container.Args,
SeccompFlags: seccomp.AllowMultiarch,
Uid: 1000,
Gid: 100,
Ops: new(container.Ops).
Root(m("/var/lib/hakurei/base/org.debian"), bits.BindWritable).
Proc(fhs.AbsProc).Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
Bind(fhs.AbsDev, fhs.AbsDev, bits.BindWritable|bits.BindDevice).
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777),
}, func(t *testing.T, state *outcomeStateParams) {
wantEnv := map[string]string{
"TERM": "xterm",
}
maps.Copy(wantEnv, config.Container.Env)
if !maps.Equal(state.env, wantEnv) {
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
}
if state.as.AutoEtcPrefix != wantAutoEtcPrefix {
t.Errorf("toContainer: as.AutoEtcPrefix = %q, want %q", state.as.AutoEtcPrefix, wantAutoEtcPrefix)
}
wantFilesystems := config.Container.Filesystem[1:]
if !reflect.DeepEqual(state.filesystem, wantFilesystems) {
t.Errorf("toContainer: filesystem = %#v, want %#v", state.filesystem, wantFilesystems)
}
}, nil},
})
}
func TestSpFilesystemOp(t *testing.T) {
const nePrefix = container.Nonexistent + "/eval"
var stubDebianRoot = stubDir("bin", "dev", "etc", "home", "lib64", "lost+found",
"mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
config := hst.Template()
newConfigSmall := func() *hst.Config {
c := hst.Template()
c.Container.Filesystem = []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: fhs.AbsEtc, Special: true}},
{FilesystemConfig: &hst.FSOverlay{Target: m("/nix/store"), Lower: []*check.Absolute{
fhs.AbsVarLib.Append("hakurei/base/org.nixos/.ro-store"),
fhs.AbsVarLib.Append("hakurei/base/org.nixos/org.chromium.Chromium"),
}}},
{FilesystemConfig: &hst.FSEphemeral{Target: hst.AbsPrivateTmp}},
}
c.Container.Device = false
return c
}
configSmall := newConfigSmall()
checkOpBehaviour(t, []opBehaviourTestCase{
{"readdir", func(bool, bool) outcomeOp {
return new(spFilesystemOp)
}, hst.Template, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{"/var/run/dbus"}, nePrefix+"/run/dbus", nil),
call("readdir", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian"}, []os.DirEntry{}, stub.UniqueError(2)),
}, nil, nil, &hst.AppError{
Step: "access autoroot source",
Err: stub.UniqueError(2),
}, nil, nil, nil, nil, nil},
{"invalid dbus address", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, func() *hst.Config {
c := newConfigSmall()
c.Container.Filesystem = append(c.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: invalidFSHost(false)})
return c
}, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid", nil),
}, nil, nil, &hst.AppError{
Step: "parse dbus address",
Err: &dbus.BadAddressError{
Type: dbus.ErrNoColon,
EntryVal: []byte("invalid"),
PairPos: -1,
},
}, nil, nil, nil, nil, nil},
{"invalid fs early", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, func() *hst.Config {
c := newConfigSmall()
c.Container.Filesystem = append(c.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: invalidFSHost(false)})
return c
}, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
}, nil, nil, &hst.AppError{
Step: "finalise",
Err: os.ErrInvalid,
Msg: "invalid filesystem at index 3",
}, nil, nil, nil, nil, nil},
{"evalSymlinks early", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, newConfigSmall, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, "", stub.UniqueError(0)),
}, nil, nil, &hst.AppError{
Step: "evaluate path hiding target",
Err: stub.UniqueError(0),
}, nil, nil, nil, nil, nil},
{"host nil abs", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, func() *hst.Config {
c := newConfigSmall()
c.Container.Filesystem = append(c.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: invalidFSHost(true)})
return c
}, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/.ro-store", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, "var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
}, nil, nil, &hst.AppError{
Step: "finalise",
Err: os.ErrInvalid,
Msg: "impossible path hiding state reached",
}, nil, nil, nil, nil, nil},
{"evalSymlinks late", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, newConfigSmall, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", stub.UniqueError(1)),
}, nil, nil, &hst.AppError{
Step: "evaluate path hiding source",
Err: stub.UniqueError(1),
}, nil, nil, nil, nil, nil},
{"invalid contains", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, newConfigSmall, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/.ro-store", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, "var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil),
}, nil, nil, &hst.AppError{
Step: "determine path hiding outcome",
Err: errors.New("Rel: can't make /proc/nonexistent/eval/xdg_runtime_dir relative to var/lib/hakurei/base/org.nixos/org.chromium.Chromium"),
}, nil, nil, nil, nil, nil},
{"invalid hide", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, newConfigSmall, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, "xdg_runtime_dir", nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, "tmp/hakurei.0", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "nscd", nil),
call("evalSymlinks", stub.ExpectArgs{"/"}, "nonexistent/dbus", nil),
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, "nonexistent", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, ".ro-store", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, "org.chromium.Chromium", nil),
call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"nonexistent/dbus", "/etc/"}}, nil, nil),
}, nil, nil, &hst.AppError{
Step: "finalise",
Err: os.ErrInvalid,
Msg: `invalid path hiding candidate "nonexistent/dbus"`,
}, nil, nil, nil, nil, nil},
{"invalid fs", func(isShim, clearUnexported bool) outcomeOp {
if !isShim {
return new(spFilesystemOp)
}
return &spFilesystemOp{HidePaths: []*check.Absolute{m("/proc/nonexistent/eval/etc/dbus")}}
}, newConfigSmall, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/.ro-store", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil),
}, newI(), nil, nil, func(state *outcomeStateParams) {
state.filesystem = configSmall.Container.Filesystem
state.params.Ops = new(container.Ops)
state.as = hst.ApplyState{AutoEtcPrefix: wantAutoEtcPrefix, Ops: opsAdapter{state.params.Ops}}
state.filesystem = append(state.filesystem, hst.FilesystemConfigJSON{})
}, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, nil, nil, &hst.AppError{
Step: "finalise",
Err: os.ErrInvalid,
Msg: "invalid filesystem at index 3",
}},
{"success noroot nodev envdbus strangedbus dbusnotabs hide", func(isShim, clearUnexported bool) outcomeOp {
if !isShim {
return new(spFilesystemOp)
}
return &spFilesystemOp{HidePaths: []*check.Absolute{m("/proc/nonexistent/eval/etc/dbus")}}
}, newConfigSmall, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/.ro-store", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil),
}, newI(), nil, nil, func(state *outcomeStateParams) {
state.filesystem = configSmall.Container.Filesystem
state.params.Ops = new(container.Ops)
state.as = hst.ApplyState{AutoEtcPrefix: wantAutoEtcPrefix, Ops: opsAdapter{state.params.Ops}}
}, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
Etc(fhs.AbsEtc, wantAutoEtcPrefix).
OverlayReadonly(
check.MustAbs("/nix/store"),
fhs.AbsVarLib.Append("hakurei/base/org.nixos/.ro-store"),
fhs.AbsVarLib.Append("hakurei/base/org.nixos/org.chromium.Chromium")).
Readonly(hst.AbsPrivateTmp, 0755).
Tmpfs(m("/proc/nonexistent/eval/etc/dbus"), 1<<13, 0755).
Remount(fhs.AbsDev, syscall.MS_RDONLY),
}, nil, nil},
{"success", func(bool, bool) outcomeOp {
return new(spFilesystemOp)
}, hst.Template, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{"/var/run/dbus"}, nePrefix+"/run/dbus", nil),
call("readdir", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian"}, stubDebianRoot, nil),
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"}, nePrefix+"/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"}, nePrefix+"/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/ro-store", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/u0/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/u0/org.chromium.Chromium", nil),
call("evalSymlinks", stub.ExpectArgs{"/dev/dri"}, nePrefix+"/dev/dri", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/bin"}, nePrefix+"/var/lib/hakurei/base/org.debian/bin", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/home"}, nePrefix+"/var/lib/hakurei/base/org.debian/home", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/lib64"}, nePrefix+"/var/lib/hakurei/base/org.debian/lib64", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/lost+found"}, nePrefix+"/var/lib/hakurei/base/org.debian/lost+found", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/nix"}, nePrefix+"/var/lib/hakurei/base/org.debian/nix", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/root"}, nePrefix+"/var/lib/hakurei/base/org.debian/root", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/run"}, nePrefix+"/var/lib/hakurei/base/org.debian/run", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/srv"}, nePrefix+"/var/lib/hakurei/base/org.debian/srv", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/sys"}, nePrefix+"/var/lib/hakurei/base/org.debian/sys", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/usr"}, nePrefix+"/var/lib/hakurei/base/org.debian/usr", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/var"}, nePrefix+"/var/lib/hakurei/base/org.debian/var", nil),
}, newI(), nil, nil, func(state *outcomeStateParams) {
state.filesystem = config.Container.Filesystem[1:]
state.params.Ops = new(container.Ops)
state.as = hst.ApplyState{AutoEtcPrefix: wantAutoEtcPrefix, Ops: opsAdapter{state.params.Ops}}
}, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
Etc(fhs.AbsEtc, wantAutoEtcPrefix).
Tmpfs(fhs.AbsTmp, 0, 0755).
Overlay(
check.MustAbs("/nix/store"),
fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"),
fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/work"),
fhs.AbsVarLib.Append("hakurei/base/org.nixos/ro-store")).
Link(fhs.AbsRun.Append("current-system"), "/run/current-system", true).
Link(fhs.AbsRun.Append("opengl-driver"), "/run/opengl-driver", true).
Bind(
fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"),
check.MustAbs("/data/data/org.chromium.Chromium"),
bits.BindWritable|bits.BindEnsure).
Bind(fhs.AbsDev.Append("dri"), fhs.AbsDev.Append("dri"), bits.BindDevice|bits.BindWritable|bits.BindOptional),
}, nil, nil},
})
}
// invalidFSHost implements the Host method of [hst.FilesystemConfig] with an invalid response.
type invalidFSHost bool
func (f invalidFSHost) Valid() bool { return bool(f) }
func (invalidFSHost) Path() *check.Absolute { panic("unreachable") }
func (invalidFSHost) Host() []*check.Absolute { return []*check.Absolute{nil} }
func (invalidFSHost) Apply(*hst.ApplyState) { panic("unreachable") }
func (invalidFSHost) String() string { panic("unreachable") }

Some files were not shown because too many files have changed in this diff Show More