Compare commits

..

No commits in common. "master" and "v0.2.0" have entirely different histories.

144 changed files with 6653 additions and 10554 deletions

View File

@ -6,9 +6,11 @@ import (
"io" "io"
"log" "log"
"os" "os"
"os/signal"
"os/user" "os/user"
"strconv" "strconv"
"sync" "sync"
"syscall"
"time" "time"
"hakurei.app/command" "hakurei.app/command"
@ -22,7 +24,7 @@ import (
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
func buildCommand(ctx context.Context, out io.Writer) command.Command { func buildCommand(out io.Writer) command.Command {
var ( var (
flagVerbose bool flagVerbose bool
flagJSON bool flagJSON bool
@ -42,35 +44,35 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
config := tryPath(args[0]) config := tryPath(args[0])
config.Args = append(config.Args, args[1:]...) config.Args = append(config.Args, args[1:]...)
app.Main(ctx, config) runApp(config)
panic("unreachable") panic("unreachable")
}) })
{ {
var ( var (
flagDBusConfigSession string dbusConfigSession string
flagDBusConfigSystem string dbusConfigSystem string
flagDBusMpris bool mpris bool
flagDBusVerbose bool dbusVerbose bool
flagID string fid string
flagIdentity int aid int
flagGroups command.RepeatableFlag groups command.RepeatableFlag
flagHomeDir string homeDir string
flagUserName string userName string
flagWayland, flagX11, flagDBus, flagPulse bool wayland, x11, dBus, pulse bool
) )
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error { c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
// initialise config from flags // initialise config from flags
config := &hst.Config{ config := &hst.Config{
ID: flagID, ID: fid,
Args: args, Args: args,
} }
if flagIdentity < 0 || flagIdentity > 9999 { if aid < 0 || aid > 9999 {
log.Fatalf("identity %d out of range", flagIdentity) log.Fatalf("aid %d out of range", aid)
} }
// resolve home/username from os when flag is unset // resolve home/username from os when flag is unset
@ -78,7 +80,14 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
passwd *user.User passwd *user.User
passwdOnce sync.Once passwdOnce sync.Once
passwdFunc = func() { passwdFunc = func() {
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustID(), flagIdentity)) var us string
if uid, err := std.Uid(aid); err != nil {
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
os.Exit(1)
} else {
us = strconv.Itoa(uid)
}
if u, err := user.LookupId(us); err != nil { if u, err := user.LookupId(us); err != nil {
hlog.Verbosef("cannot look up uid %s", us) hlog.Verbosef("cannot look up uid %s", us)
passwd = &user.User{ passwd = &user.User{
@ -94,21 +103,21 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
} }
) )
if flagHomeDir == "os" { if homeDir == "os" {
passwdOnce.Do(passwdFunc) passwdOnce.Do(passwdFunc)
flagHomeDir = passwd.HomeDir homeDir = passwd.HomeDir
} }
if flagUserName == "chronos" { if userName == "chronos" {
passwdOnce.Do(passwdFunc) passwdOnce.Do(passwdFunc)
flagUserName = passwd.Username userName = passwd.Username
} }
config.Identity = flagIdentity config.Identity = aid
config.Groups = flagGroups config.Groups = groups
config.Username = flagUserName config.Username = userName
if a, err := container.NewAbs(flagHomeDir); err != nil { if a, err := container.NewAbs(homeDir); err != nil {
log.Fatal(err.Error()) log.Fatal(err.Error())
return err return err
} else { } else {
@ -116,114 +125,105 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
} }
var e system.Enablement var e system.Enablement
if flagWayland { if wayland {
e |= system.EWayland e |= system.EWayland
} }
if flagX11 { if x11 {
e |= system.EX11 e |= system.EX11
} }
if flagDBus { if dBus {
e |= system.EDBus e |= system.EDBus
} }
if flagPulse { if pulse {
e |= system.EPulse e |= system.EPulse
} }
config.Enablements = hst.NewEnablements(e) config.Enablements = hst.NewEnablements(e)
// parse D-Bus config file from flags if applicable // parse D-Bus config file from flags if applicable
if flagDBus { if dBus {
if flagDBusConfigSession == "builtin" { if dbusConfigSession == "builtin" {
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris) config.SessionBus = dbus.NewConfig(fid, true, mpris)
} else { } else {
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSession); err != nil { if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
log.Fatalf("cannot load session bus proxy config from %q: %s", flagDBusConfigSession, err) log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else { } else {
config.SessionBus = conf config.SessionBus = conf
} }
} }
// system bus proxy is optional // system bus proxy is optional
if flagDBusConfigSystem != "nil" { if dbusConfigSystem != "nil" {
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSystem); err != nil { if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
log.Fatalf("cannot load system bus proxy config from %q: %s", flagDBusConfigSystem, err) log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else { } else {
config.SystemBus = conf config.SystemBus = conf
} }
} }
// override log from configuration // override log from configuration
if flagDBusVerbose { if dbusVerbose {
if config.SessionBus != nil { config.SessionBus.Log = true
config.SessionBus.Log = true config.SystemBus.Log = true
}
if config.SystemBus != nil {
config.SystemBus.Log = true
}
} }
} }
app.Main(ctx, config) // invoke app
runApp(config)
panic("unreachable") panic("unreachable")
}). }).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"), Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
"Path to session bus proxy config file, or \"builtin\" for defaults"). "Path to session bus proxy config file, or \"builtin\" for defaults").
Flag(&flagDBusConfigSystem, "dbus-system", command.StringFlag("nil"), Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"),
"Path to system bus proxy config file, or \"nil\" to disable"). "Path to system bus proxy config file, or \"nil\" to disable").
Flag(&flagDBusMpris, "mpris", command.BoolFlag(false), Flag(&mpris, "mpris", command.BoolFlag(false),
"Allow owning MPRIS D-Bus path, has no effect if custom config is available"). "Allow owning MPRIS D-Bus path, has no effect if custom config is available").
Flag(&flagDBusVerbose, "dbus-log", command.BoolFlag(false), Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false),
"Force buffered logging in the D-Bus proxy"). "Force buffered logging in the D-Bus proxy").
Flag(&flagID, "id", command.StringFlag(""), Flag(&fid, "id", command.StringFlag(""),
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier"). "Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
Flag(&flagIdentity, "a", command.IntFlag(0), Flag(&aid, "a", command.IntFlag(0),
"Application identity"). "Application identity").
Flag(nil, "g", &flagGroups, Flag(nil, "g", &groups,
"Groups inherited by all container processes"). "Groups inherited by all container processes").
Flag(&flagHomeDir, "d", command.StringFlag("os"), Flag(&homeDir, "d", command.StringFlag("os"),
"Container home directory"). "Container home directory").
Flag(&flagUserName, "u", command.StringFlag("chronos"), Flag(&userName, "u", command.StringFlag("chronos"),
"Passwd user name within sandbox"). "Passwd user name within sandbox").
Flag(&flagWayland, "wayland", command.BoolFlag(false), Flag(&wayland, "wayland", command.BoolFlag(false),
"Enable connection to Wayland via security-context-v1"). "Enable connection to Wayland via security-context-v1").
Flag(&flagX11, "X", command.BoolFlag(false), Flag(&x11, "X", command.BoolFlag(false),
"Enable direct connection to X11"). "Enable direct connection to X11").
Flag(&flagDBus, "dbus", command.BoolFlag(false), Flag(&dBus, "dbus", command.BoolFlag(false),
"Enable proxied connection to D-Bus"). "Enable proxied connection to D-Bus").
Flag(&flagPulse, "pulse", command.BoolFlag(false), Flag(&pulse, "pulse", command.BoolFlag(false),
"Enable direct connection to PulseAudio") "Enable direct connection to PulseAudio")
} }
{ var showFlagShort bool
var flagShort bool c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
c.NewCommand("show", "Show live or local app configuration", func(args []string) error { switch len(args) {
switch len(args) { case 0: // system
case 0: // system printShowSystem(os.Stdout, showFlagShort, flagJSON)
printShowSystem(os.Stdout, flagShort, flagJSON)
case 1: // instance case 1: // instance
name := args[0] name := args[0]
config, entry := tryShort(name) config, entry := tryShort(name)
if config == nil { if config == nil {
config = tryPath(name) config = tryPath(name)
}
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON)
default:
log.Fatal("show requires 1 argument")
} }
return errSuccess printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
}).Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information")
}
{ default:
var flagShort bool log.Fatal("show requires 1 argument")
c.NewCommand("ps", "List active instances", func(args []string) error { }
var sc hst.Paths return errSuccess
app.CopyPaths(&sc, new(app.Hsu).MustID()) }).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sc.RunDirPath.String()), flagShort, flagJSON)
return errSuccess var psFlagShort bool
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id") c.NewCommand("ps", "List active instances", func(args []string) error {
} printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), psFlagShort, flagJSON)
return errSuccess
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
c.Command("version", "Display version information", func(args []string) error { c.Command("version", "Display version information", func(args []string) error {
fmt.Println(internal.Version()) fmt.Println(internal.Version())
@ -247,3 +247,20 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
return c return c
} }
func runApp(config *hst.Config) {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
a := app.MustNew(ctx, std)
rs := new(app.RunState)
if sa, err := a.Seal(config); err != nil {
hlog.PrintBaseError(err, "cannot seal app:")
internal.Exit(1)
} else {
internal.Exit(app.PrintRunStateErr(rs, sa.Run(rs)))
}
*(*int)(nil) = 0 // not reached
}

View File

@ -68,7 +68,7 @@ Flags:
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
out := new(bytes.Buffer) out := new(bytes.Buffer)
c := buildCommand(t.Context(), out) c := buildCommand(out)
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) { if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
t.Errorf("Parse: error = %v; want %v", t.Errorf("Parse: error = %v; want %v",
err, command.ErrHelp) err, command.ErrHelp)

View File

@ -4,17 +4,15 @@ package main
//go:generate cp ../../LICENSE . //go:generate cp ../../LICENSE .
import ( import (
"context"
_ "embed" _ "embed"
"errors" "errors"
"log" "log"
"os" "os"
"os/signal"
"syscall"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
) )
var ( var (
@ -26,6 +24,8 @@ var (
func init() { hlog.Prepare("hakurei") } func init() { hlog.Prepare("hakurei") }
var std sys.State = new(sys.Std)
func main() { func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE // early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
@ -44,11 +44,7 @@ func main() {
log.Fatal("this program must not run as root") log.Fatal("this program must not run as root")
} }
ctx, stop := signal.NotifyContext(context.Background(), buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
buildCommand(ctx, os.Stderr).MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err) hlog.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) { if errors.Is(err, errSuccess) {
hlog.BeforeExit() hlog.BeforeExit()

View File

@ -11,7 +11,6 @@ import (
"syscall" "syscall"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
) )
@ -88,9 +87,7 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
if likePrefix && len(name) >= 8 { if likePrefix && len(name) >= 8 {
hlog.Verbose("argument looks like prefix") hlog.Verbose("argument looks like prefix")
var sc hst.Paths s := state.NewMulti(std.Paths().RunDirPath.String())
app.CopyPaths(&sc, new(app.Hsu).MustID())
s := state.NewMulti(sc.RunDirPath.String())
if entries, err := state.Join(s); err != nil { if entries, err := state.Join(s); err != nil {
log.Printf("cannot join store: %v", err) log.Printf("cannot join store: %v", err)
// drop to fetch from file // drop to fetch from file

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"os"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@ -12,8 +13,8 @@ import (
"time" "time"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -21,8 +22,15 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
info := &hst.Info{User: new(app.Hsu).MustID()} info := &hst.Info{Paths: std.Paths()}
app.CopyPaths(&info.Paths, info.User)
// get hid by querying uid of identity 0
if uid, err := std.Uid(0); err != nil {
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
os.Exit(1)
} else {
info.User = (uid / 10000) - 100
}
if flagJSON { if flagJSON {
printJSON(output, short, info) printJSON(output, short, info)

View File

@ -15,7 +15,7 @@ import (
const ( const (
hsuConfFile = "/etc/hsurc" hsuConfFile = "/etc/hsurc"
envShim = "HAKUREI_SHIM" envShim = "HAKUREI_SHIM"
envIdentity = "HAKUREI_IDENTITY" envAID = "HAKUREI_APP_ID"
envGroups = "HAKUREI_GROUPS" envGroups = "HAKUREI_GROUPS"
PR_SET_NO_NEW_PRIVS = 0x26 PR_SET_NO_NEW_PRIVS = 0x26
@ -48,8 +48,8 @@ func main() {
} }
// uid = 1000000 + // uid = 1000000 +
// id * 10000 + // fid * 10000 +
// identity // aid
uid := 1000000 uid := 1000000
// refuse to run if hsurc is not protected correctly // refuse to run if hsurc is not protected correctly
@ -62,25 +62,29 @@ func main() {
} }
// authenticate before accepting user input // authenticate before accepting user input
var id int
if f, err := os.Open(hsuConfFile); err != nil { if f, err := os.Open(hsuConfFile); err != nil {
log.Fatal(err) log.Fatal(err)
} else if v, ok := mustParseConfig(f, puid); !ok { } else if fid, ok := mustParseConfig(f, puid); !ok {
log.Fatalf("uid %d is not in the hsurc file", puid) log.Fatalf("uid %d is not in the hsurc file", puid)
} else { } else {
id = v uid += fid * 10000
if err = f.Close(); err != nil { }
log.Fatal(err)
}
uid += id * 10000 // allowed aid range 0 to 9999
if as, ok := os.LookupEnv(envAID); !ok {
log.Fatal("HAKUREI_APP_ID not set")
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
log.Fatal("invalid aid")
} else {
uid += aid
} }
// pass through setup fd to shim // pass through setup fd to shim
var shimSetupFd string var shimSetupFd string
if s, ok := os.LookupEnv(envShim); !ok { if s, ok := os.LookupEnv(envShim); !ok {
// hakurei requests hsurc user id // hakurei requests target uid
fmt.Print(id) // print resolved uid and exit
fmt.Print(uid)
os.Exit(0) os.Exit(0)
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' { } else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
log.Fatal("HAKUREI_SHIM holds an invalid value") log.Fatal("HAKUREI_SHIM holds an invalid value")
@ -88,15 +92,6 @@ func main() {
shimSetupFd = s shimSetupFd = s
} }
// allowed identity range 0 to 9999
if as, ok := os.LookupEnv(envIdentity); !ok {
log.Fatal("HAKUREI_IDENTITY not set")
} else if identity, err := parseUint32Fast(as); err != nil || identity < 0 || identity > 9999 {
log.Fatal("invalid identity")
} else {
uid += identity
}
// supplementary groups // supplementary groups
var suppGroups, suppCurrent []int var suppGroups, suppCurrent []int

View File

@ -3,6 +3,7 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"io/fs"
) )
func init() { gob.Register(new(AutoEtcOp)) } func init() { gob.Register(new(AutoEtcOp)) }
@ -23,7 +24,7 @@ func (e *AutoEtcOp) Valid() bool { return e != ni
func (e *AutoEtcOp) early(*setupState, syscallDispatcher) error { return nil } func (e *AutoEtcOp) early(*setupState, syscallDispatcher) error { return nil }
func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error { func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
if state.nonrepeatable&nrAutoEtc != 0 { if state.nonrepeatable&nrAutoEtc != 0 {
return OpRepeatError("autoetc") return msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
} }
state.nonrepeatable |= nrAutoEtc state.nonrepeatable |= nrAutoEtc
@ -31,10 +32,10 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
rel := e.hostRel() + "/" rel := e.hostRel() + "/"
if err := k.mkdirAll(target, 0755); err != nil { if err := k.mkdirAll(target, 0755); err != nil {
return err return wrapErrSelf(err)
} }
if d, err := k.readdir(toSysroot(e.hostPath().String())); err != nil { if d, err := k.readdir(toSysroot(e.hostPath().String())); err != nil {
return err return wrapErrSelf(err)
} else { } else {
for _, ent := range d { for _, ent := range d {
n := ent.Name() n := ent.Name()
@ -43,12 +44,12 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
case "mtab": case "mtab":
if err = k.symlink(FHSProc+"mounts", target+n); err != nil { if err = k.symlink(FHSProc+"mounts", target+n); err != nil {
return err return wrapErrSelf(err)
} }
default: default:
if err = k.symlink(rel+n, target+n); err != nil { if err = k.symlink(rel+n, target+n); err != nil {
return err return wrapErrSelf(err)
} }
} }
} }
@ -64,5 +65,5 @@ func (e *AutoEtcOp) Is(op Op) bool {
ve, ok := op.(*AutoEtcOp) ve, ok := op.(*AutoEtcOp)
return ok && e.Valid() && ve.Valid() && *e == *ve return ok && e.Valid() && ve.Valid() && *e == *ve
} }
func (*AutoEtcOp) prefix() (string, bool) { return "setting up", true } func (*AutoEtcOp) prefix() string { return "setting up" }
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) } func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }

View File

@ -2,15 +2,14 @@ package container
import ( import (
"errors" "errors"
"io/fs"
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestAutoEtcOp(t *testing.T) { func TestAutoEtcOp(t *testing.T) {
t.Run("nonrepeatable", func(t *testing.T) { t.Run("nonrepeatable", func(t *testing.T) {
wantErr := OpRepeatError("autoetc") wantErr := msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) { if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr) t.Errorf("apply: error = %v, want %v", err, wantErr)
} }
@ -19,22 +18,22 @@ func TestAutoEtcOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{ checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdirAll", new(Params), &AutoEtcOp{ {"mkdirAll", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9", Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, stub.UniqueError(3)), {"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, errUnique},
}, stub.UniqueError(3)}, }, wrapErrSelf(errUnique)},
{"readdir", new(Params), &AutoEtcOp{ {"readdir", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9", Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), stub.UniqueError(2)), {"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), errUnique},
}, stub.UniqueError(2)}, }, wrapErrSelf(errUnique)},
{"symlink", new(Params), &AutoEtcOp{ {"symlink", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9", Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host", {"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts", "alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd", "fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf", "locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
@ -42,15 +41,15 @@ func TestAutoEtcOp(t *testing.T) {
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile", "nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells", "protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo", "ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil), "tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, stub.UniqueError(1)), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, errUnique},
}, stub.UniqueError(1)}, }, wrapErrSelf(errUnique)},
{"symlink mtab", new(Params), &AutoEtcOp{ {"symlink mtab", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9", Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host", {"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts", "alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd", "fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf", "locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
@ -58,41 +57,41 @@ func TestAutoEtcOp(t *testing.T) {
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile", "nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells", "protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo", "ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil), "tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, stub.UniqueError(0)), {"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, errUnique},
}, stub.UniqueError(0)}, }, wrapErrSelf(errUnique)},
{"success nested", new(Params), &AutoEtcOp{ {"success nested", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9", Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host", {"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts", "alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd", "fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf", "locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
@ -100,79 +99,79 @@ func TestAutoEtcOp(t *testing.T) {
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile", "nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells", "protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo", "ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil), "tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil), {"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil},
}, nil}, }, nil},
{"success", new(Params), &AutoEtcOp{ {"success", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9", Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir( {"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts", "alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd", "fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf", "locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
@ -180,72 +179,72 @@ func TestAutoEtcOp(t *testing.T) {
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile", "nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells", "protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo", "ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil), "tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil), {"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil},
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil), {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil},
}, nil}, }, nil},
}) })

View File

@ -3,6 +3,7 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"io/fs"
) )
func init() { gob.Register(new(AutoRootOp)) } func init() { gob.Register(new(AutoRootOp)) }
@ -22,20 +23,19 @@ type AutoRootOp struct {
// obtained during early; // obtained during early;
// these wrap the underlying Op because BindMountOp is relatively complex, // these wrap the underlying Op because BindMountOp is relatively complex,
// so duplicating that code would be unwise // so duplicating that code would be unwise
resolved []*BindMountOp resolved []Op
} }
func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil } func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil }
func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error { func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
if d, err := k.readdir(r.Host.String()); err != nil { if d, err := k.readdir(r.Host.String()); err != nil {
return err return wrapErrSelf(err)
} else { } else {
r.resolved = make([]*BindMountOp, 0, len(d)) r.resolved = make([]Op, 0, len(d))
for _, ent := range d { for _, ent := range d {
name := ent.Name() name := ent.Name()
if IsAutoRootBindable(name) { if IsAutoRootBindable(name) {
// careful: the Valid method is skipped, make sure this is always valid
op := &BindMountOp{ op := &BindMountOp{
Source: r.Host.Append(name), Source: r.Host.Append(name),
Target: AbsFHSRoot.Append(name), Target: AbsFHSRoot.Append(name),
@ -53,12 +53,12 @@ func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error { func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
if state.nonrepeatable&nrAutoRoot != 0 { if state.nonrepeatable&nrAutoRoot != 0 {
return OpRepeatError("autoroot") return msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
} }
state.nonrepeatable |= nrAutoRoot state.nonrepeatable |= nrAutoRoot
for _, op := range r.resolved { for _, op := range r.resolved {
// these are exclusively BindMountOp, do not attempt to print identifying message k.verbosef("%s %s", op.prefix(), op)
if err := op.apply(state, k); err != nil { if err := op.apply(state, k); err != nil {
return err return err
} }
@ -72,7 +72,7 @@ func (r *AutoRootOp) Is(op Op) bool {
r.Host.Is(vr.Host) && r.Host.Is(vr.Host) &&
r.Flags == vr.Flags r.Flags == vr.Flags
} }
func (*AutoRootOp) prefix() (string, bool) { return "setting up", true } func (*AutoRootOp) prefix() string { return "setting up" }
func (r *AutoRootOp) String() string { func (r *AutoRootOp) String() string {
return fmt.Sprintf("auto root %q flags %#x", r.Host, r.Flags) return fmt.Sprintf("auto root %q flags %#x", r.Host, r.Flags)
} }

View File

@ -2,15 +2,14 @@ package container
import ( import (
"errors" "errors"
"io/fs"
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestAutoRootOp(t *testing.T) { func TestAutoRootOp(t *testing.T) {
t.Run("nonrepeatable", func(t *testing.T) { t.Run("nonrepeatable", func(t *testing.T) {
wantErr := OpRepeatError("autoroot") wantErr := msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) { if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr) t.Errorf("apply: error = %v, want %v", err, wantErr)
} }
@ -20,99 +19,100 @@ func TestAutoRootOp(t *testing.T) {
{"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{ {"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"), Host: MustAbs("/"),
Flags: BindWritable, Flags: BindWritable,
}, []stub.Call{ }, []kexpect{
call("readdir", stub.ExpectArgs{"/"}, stubDir(), stub.UniqueError(2)), {"readdir", expectArgs{"/"}, stubDir(), errUnique},
}, stub.UniqueError(2), nil, nil}, }, wrapErrSelf(errUnique), nil, nil},
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{ {"early", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"), Host: MustAbs("/"),
Flags: BindWritable, Flags: BindWritable,
}, []stub.Call{ }, []kexpect{
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64", {"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil), "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "", stub.UniqueError(1)), {"evalSymlinks", expectArgs{"/bin"}, "", errUnique},
}, stub.UniqueError(1), nil, nil}, }, wrapErrSelf(errUnique), nil, nil},
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{ {"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"), Host: MustAbs("/"),
Flags: BindWritable, Flags: BindWritable,
}, []stub.Call{ }, []kexpect{
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64", {"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil), "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "/usr/bin", nil), {"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil},
call("evalSymlinks", stub.ExpectArgs{"/home"}, "/home", nil), {"evalSymlinks", expectArgs{"/home"}, "/home", nil},
call("evalSymlinks", stub.ExpectArgs{"/lib64"}, "/lib64", nil), {"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil},
call("evalSymlinks", stub.ExpectArgs{"/lost+found"}, "/lost+found", nil), {"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil},
call("evalSymlinks", stub.ExpectArgs{"/nix"}, "/nix", nil), {"evalSymlinks", expectArgs{"/nix"}, "/nix", nil},
call("evalSymlinks", stub.ExpectArgs{"/root"}, "/root", nil), {"evalSymlinks", expectArgs{"/root"}, "/root", nil},
call("evalSymlinks", stub.ExpectArgs{"/run"}, "/run", nil), {"evalSymlinks", expectArgs{"/run"}, "/run", nil},
call("evalSymlinks", stub.ExpectArgs{"/srv"}, "/srv", nil), {"evalSymlinks", expectArgs{"/srv"}, "/srv", nil},
call("evalSymlinks", stub.ExpectArgs{"/sys"}, "/sys", nil), {"evalSymlinks", expectArgs{"/sys"}, "/sys", nil},
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil), {"evalSymlinks", expectArgs{"/usr"}, "/usr", nil},
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil), {"evalSymlinks", expectArgs{"/var"}, "/var", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(false), stub.UniqueError(0)), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil},
}, stub.UniqueError(0)}, {"stat", expectArgs{"/host/usr/bin"}, isDirFi(false), errUnique},
}, wrapErrSelf(errUnique)},
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{ {"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"), Host: MustAbs("/"),
Flags: BindWritable, Flags: BindWritable,
}, []stub.Call{ }, []kexpect{
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64", {"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil), "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "/usr/bin", nil), {"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil},
call("evalSymlinks", stub.ExpectArgs{"/home"}, "/home", nil), {"evalSymlinks", expectArgs{"/home"}, "/home", nil},
call("evalSymlinks", stub.ExpectArgs{"/lib64"}, "/lib64", nil), {"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil},
call("evalSymlinks", stub.ExpectArgs{"/lost+found"}, "/lost+found", nil), {"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil},
call("evalSymlinks", stub.ExpectArgs{"/nix"}, "/nix", nil), {"evalSymlinks", expectArgs{"/nix"}, "/nix", nil},
call("evalSymlinks", stub.ExpectArgs{"/root"}, "/root", nil), {"evalSymlinks", expectArgs{"/root"}, "/root", nil},
call("evalSymlinks", stub.ExpectArgs{"/run"}, "/run", nil), {"evalSymlinks", expectArgs{"/run"}, "/run", nil},
call("evalSymlinks", stub.ExpectArgs{"/srv"}, "/srv", nil), {"evalSymlinks", expectArgs{"/srv"}, "/srv", nil},
call("evalSymlinks", stub.ExpectArgs{"/sys"}, "/sys", nil), {"evalSymlinks", expectArgs{"/sys"}, "/sys", nil},
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil), {"evalSymlinks", expectArgs{"/usr"}, "/usr", nil},
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil), {"evalSymlinks", expectArgs{"/var"}, "/var", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/home", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/home"), MustAbs("/home"), MustAbs("/home"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/lib64", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lib64"), MustAbs("/lib64"), MustAbs("/lib64"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/lost+found", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lost+found"), MustAbs("/lost+found"), MustAbs("/lost+found"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/nix", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/nix"), MustAbs("/nix"), MustAbs("/nix"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/root", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/root"), MustAbs("/root"), MustAbs("/root"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/run", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/run"), MustAbs("/run"), MustAbs("/run"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/srv", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/srv"), MustAbs("/srv"), MustAbs("/srv"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/sys", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/sys"), MustAbs("/sys"), MustAbs("/sys"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/usr", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr"), MustAbs("/usr"), MustAbs("/usr"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/var", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var"), MustAbs("/var"), MustAbs("/var"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil},
}, nil}, }, nil},
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{ {"success", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"), Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"),
}, []stub.Call{ }, []kexpect{
call("readdir", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64", {"readdir", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil), "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/bin"}, "/var/lib/planterette/base/debian:f92c9052/usr/bin", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/bin"}, "/var/lib/planterette/base/debian:f92c9052/usr/bin", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/home"}, "/var/lib/planterette/base/debian:f92c9052/home", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/home"}, "/var/lib/planterette/base/debian:f92c9052/home", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/lib64"}, "/var/lib/planterette/base/debian:f92c9052/lib64", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lib64"}, "/var/lib/planterette/base/debian:f92c9052/lib64", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/lost+found"}, "/var/lib/planterette/base/debian:f92c9052/lost+found", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lost+found"}, "/var/lib/planterette/base/debian:f92c9052/lost+found", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/nix"}, "/var/lib/planterette/base/debian:f92c9052/nix", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/nix"}, "/var/lib/planterette/base/debian:f92c9052/nix", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/root"}, "/var/lib/planterette/base/debian:f92c9052/root", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/root"}, "/var/lib/planterette/base/debian:f92c9052/root", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/run"}, "/var/lib/planterette/base/debian:f92c9052/run", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/run"}, "/var/lib/planterette/base/debian:f92c9052/run", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/srv"}, "/var/lib/planterette/base/debian:f92c9052/srv", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/srv"}, "/var/lib/planterette/base/debian:f92c9052/srv", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/sys"}, "/var/lib/planterette/base/debian:f92c9052/sys", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/sys"}, "/var/lib/planterette/base/debian:f92c9052/sys", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr/bin"), MustAbs("/var/lib/planterette/base/debian:f92c9052/bin"), MustAbs("/bin"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/home"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/lib64"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/lost+found"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/nix"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/root"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/run"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/srv"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/sys"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/usr"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil},
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil), {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil},
}, nil}, }, nil},
}) })
@ -140,7 +140,7 @@ func TestAutoRootOp(t *testing.T) {
}, &AutoRootOp{ }, &AutoRootOp{
Host: MustAbs("/"), Host: MustAbs("/"),
Flags: BindWritable, Flags: BindWritable,
resolved: []*BindMountOp{new(BindMountOp)}, resolved: []Op{new(BindMountOp)},
}, true}, }, true},
{"flags differs", &AutoRootOp{ {"flags differs", &AutoRootOp{

View File

@ -64,7 +64,7 @@ type (
Args []string Args []string
// Deliver SIGINT to the initial process on context cancellation. // Deliver SIGINT to the initial process on context cancellation.
ForwardCancel bool ForwardCancel bool
// Time to wait for processes lingering after the initial process terminates. // time to wait for linger processes after death of initial process
AdoptWaitDelay time.Duration AdoptWaitDelay time.Duration
// Mapped Uid in user namespace. // Mapped Uid in user namespace.
@ -99,66 +99,17 @@ type (
} }
) )
// A StartError contains additional information on a container startup failure.
type StartError struct {
// Fatal suggests whether this error should be considered fatal for the entire program.
Fatal bool
// Step refers to the part of the setup this error is returned from.
Step string
// Err is the underlying error.
Err error
// Origin is whether this error originated from the [Container.Start] method.
Origin bool
// Passthrough is whether the Error method is passed through to Err.
Passthrough bool
}
func (e *StartError) Unwrap() error { return e.Err }
func (e *StartError) Error() string {
if e.Passthrough {
return e.Err.Error()
}
if e.Origin {
return e.Step
}
{
var syscallError *os.SyscallError
if errors.As(e.Err, &syscallError) && syscallError != nil {
return e.Step + " " + syscallError.Error()
}
}
return e.Step + ": " + e.Err.Error()
}
// Message returns a user-facing error message.
func (e *StartError) Message() string {
if e.Passthrough {
switch {
case errors.As(e.Err, new(*os.PathError)),
errors.As(e.Err, new(*os.SyscallError)):
return "cannot " + e.Err.Error()
default:
return e.Err.Error()
}
}
if e.Origin {
return e.Step
}
return "cannot " + e.Error()
}
// Start starts the container init. The init process blocks until Serve is called. // Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error { func (p *Container) Start() error {
if p == nil || p.cmd == nil || if p.cmd != nil {
p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("container: starting an invalid container")
}
if p.cmd.Process != nil {
return errors.New("container: already started") return errors.New("container: already started")
} }
if p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("container: starting an empty container")
}
ctx, cancel := context.WithCancel(p.ctx)
p.cancel = cancel
// map to overflow id to work around ownership checks // map to overflow id to work around ownership checks
if p.Uid < 1 { if p.Uid < 1 {
@ -180,17 +131,9 @@ func (p *Container) Start() error {
p.AdoptWaitDelay = 0 p.AdoptWaitDelay = 0
} }
if p.cmd.Stdin == nil { p.cmd = exec.CommandContext(ctx, MustExecutable())
p.cmd.Stdin = p.Stdin
}
if p.cmd.Stdout == nil {
p.cmd.Stdout = p.Stdout
}
if p.cmd.Stderr == nil {
p.cmd.Stderr = p.Stderr
}
p.cmd.Args = []string{initName} p.cmd.Args = []string{initName}
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil { if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) } p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
@ -224,7 +167,8 @@ func (p *Container) Start() error {
// place setup pipe before user supplied extra files, this is later restored by init // place setup pipe before user supplied extra files, this is later restored by init
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil { if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
return &StartError{true, "set up params stream", err, false, false} return wrapErrSuffix(err,
"cannot create shim setup pipe:")
} else { } else {
p.setup = e p.setup = e
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)} p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
@ -239,7 +183,8 @@ func (p *Container) Start() error {
done <- func() error { // setup depending on per-thread state must happen here done <- func() error { // setup depending on per-thread state must happen here
// PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread // PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread
if err := SetNoNewPrivs(); err != nil { if err := SetNoNewPrivs(); err != nil {
return &StartError{true, "prctl(PR_SET_NO_NEW_PRIVS)", err, false, false} return wrapErrSuffix(err,
"prctl(PR_SET_NO_NEW_PRIVS):")
} }
// landlock: depends on per-thread state but acts on a process group // landlock: depends on per-thread state but acts on a process group
@ -255,24 +200,28 @@ func (p *Container) Start() error {
// already covered by namespaces (pid) // already covered by namespaces (pid)
goto landlockOut goto landlockOut
} }
return &StartError{false, "get landlock ABI", err, false, false} return wrapErrSuffix(err,
"landlock does not appear to be enabled:")
} else if abi < 6 { } else if abi < 6 {
if p.HostAbstract { if p.HostAbstract {
// see above comment // see above comment
goto landlockOut goto landlockOut
} }
return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false} return msg.WrapErr(ENOSYS,
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET")
} else { } else {
msg.Verbosef("landlock abi version %d", abi) msg.Verbosef("landlock abi version %d", abi)
} }
if rulesetFd, err := rulesetAttr.Create(0); err != nil { if rulesetFd, err := rulesetAttr.Create(0); err != nil {
return &StartError{true, "create landlock ruleset", err, false, false} return wrapErrSuffix(err,
"cannot create landlock ruleset:")
} else { } else {
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr) msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil { if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd) _ = Close(rulesetFd)
return &StartError{true, "enforce landlock ruleset", err, false, false} return wrapErrSuffix(err,
"cannot enforce landlock ruleset:")
} }
if err = Close(rulesetFd); err != nil { if err = Close(rulesetFd); err != nil {
msg.Verbosef("cannot close landlock ruleset: %v", err) msg.Verbosef("cannot close landlock ruleset: %v", err)
@ -285,7 +234,7 @@ func (p *Container) Start() error {
msg.Verbose("starting container init") msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil { if err := p.cmd.Start(); err != nil {
return &StartError{false, "start container init", err, false, true} return msg.WrapErr(err, err.Error())
} }
return nil return nil
}() }()
@ -308,7 +257,7 @@ func (p *Container) Serve() error {
if p.Path == nil { if p.Path == nil {
p.cancel() p.cancel()
return &StartError{false, "invalid executable pathname", EINVAL, true, false} return msg.WrapErr(EINVAL, "invalid executable pathname")
} }
// do not transmit nil // do not transmit nil
@ -336,7 +285,7 @@ func (p *Container) Serve() error {
// Wait waits for the container init process to exit and releases any resources associated with the [Container]. // Wait waits for the container init process to exit and releases any resources associated with the [Container].
func (p *Container) Wait() error { func (p *Container) Wait() error {
if p.cmd == nil || p.cmd.Process == nil { if p.cmd == nil {
return EINVAL return EINVAL
} }
@ -348,36 +297,6 @@ func (p *Container) Wait() error {
return err return err
} }
// StdinPipe calls the [exec.Cmd] method with the same name.
func (p *Container) StdinPipe() (w io.WriteCloser, err error) {
if p.Stdin != nil {
return nil, errors.New("container: Stdin already set")
}
w, err = p.cmd.StdinPipe()
p.Stdin = p.cmd.Stdin
return
}
// StdoutPipe calls the [exec.Cmd] method with the same name.
func (p *Container) StdoutPipe() (r io.ReadCloser, err error) {
if p.Stdout != nil {
return nil, errors.New("container: Stdout already set")
}
r, err = p.cmd.StdoutPipe()
p.Stdout = p.cmd.Stdout
return
}
// StderrPipe calls the [exec.Cmd] method with the same name.
func (p *Container) StderrPipe() (r io.ReadCloser, err error) {
if p.Stderr != nil {
return nil, errors.New("container: Stderr already set")
}
r, err = p.cmd.StderrPipe()
p.Stderr = p.cmd.Stderr
return
}
func (p *Container) String() string { func (p *Container) String() string {
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x", return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets)) p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
@ -393,11 +312,7 @@ func (p *Container) ProcessState() *os.ProcessState {
// New returns the address to a new instance of [Container] that requires further initialisation before use. // New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context) *Container { func New(ctx context.Context) *Container {
p := &Container{ctx: ctx, Params: Params{Ops: new(Ops)}} return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
c, cancel := context.WithCancel(ctx)
p.cancel = cancel
p.cmd = exec.CommandContext(c, MustExecutable())
return p
} }
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields. // NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.

View File

@ -7,11 +7,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"net"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"reflect"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@ -28,143 +26,6 @@ import (
"hakurei.app/ldd" "hakurei.app/ldd"
) )
func TestStartError(t *testing.T) {
testCases := []struct {
name string
err error
s string
is error
isF error
msg string
}{
{"params env", &container.StartError{
Fatal: true,
Step: "set up params stream",
Err: container.ErrReceiveEnv,
},
"set up params stream: environment variable not set",
container.ErrReceiveEnv, syscall.EBADF,
"cannot set up params stream: environment variable not set"},
{"params", &container.StartError{
Fatal: true,
Step: "set up params stream",
Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF},
},
"set up params stream pipe2: bad file descriptor",
syscall.EBADF, os.ErrInvalid,
"cannot set up params stream pipe2: bad file descriptor"},
{"PR_SET_NO_NEW_PRIVS", &container.StartError{
Fatal: true,
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
Err: syscall.EPERM,
},
"prctl(PR_SET_NO_NEW_PRIVS): operation not permitted",
syscall.EPERM, syscall.EACCES,
"cannot prctl(PR_SET_NO_NEW_PRIVS): operation not permitted"},
{"landlock abi", &container.StartError{
Step: "get landlock ABI",
Err: syscall.ENOSYS,
},
"get landlock ABI: function not implemented",
syscall.ENOSYS, syscall.ENOEXEC,
"cannot get landlock ABI: function not implemented"},
{"landlock old", &container.StartError{
Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
Err: syscall.ENOSYS,
Origin: true,
},
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
syscall.ENOSYS, syscall.ENOSPC,
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"},
{"landlock create", &container.StartError{
Fatal: true,
Step: "create landlock ruleset",
Err: syscall.EBADFD,
},
"create landlock ruleset: file descriptor in bad state",
syscall.EBADFD, syscall.EBADF,
"cannot create landlock ruleset: file descriptor in bad state"},
{"landlock enforce", &container.StartError{
Fatal: true,
Step: "enforce landlock ruleset",
Err: syscall.ENOTRECOVERABLE,
},
"enforce landlock ruleset: state not recoverable",
syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT,
"cannot enforce landlock ruleset: state not recoverable"},
{"start", &container.StartError{
Step: "start container init",
Err: &os.PathError{
Op: "fork/exec",
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
}, Passthrough: true,
},
"fork/exec /proc/nonexistent: no such file or directory",
syscall.ENOENT, syscall.ENOSYS,
"cannot fork/exec /proc/nonexistent: no such file or directory"},
{"start syscall", &container.StartError{
Step: "start container init",
Err: &os.SyscallError{
Syscall: "open",
Err: syscall.ENOSYS,
}, Passthrough: true,
},
"open: function not implemented",
syscall.ENOSYS, syscall.ENOENT,
"cannot open: function not implemented"},
{"start other", &container.StartError{
Step: "start container init",
Err: &net.OpError{
Op: "dial",
Net: "unix",
Err: syscall.ECONNREFUSED,
}, Passthrough: true,
},
"dial unix: connection refused",
syscall.ECONNREFUSED, syscall.ECONNABORTED,
"dial unix: connection refused"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.s {
t.Errorf("Error: %q, want %q", got, tc.s)
}
})
t.Run("is", func(t *testing.T) {
if !errors.Is(tc.err, tc.is) {
t.Error("Is: unexpected false")
}
if errors.Is(tc.err, tc.isF) {
t.Errorf("Is: unexpected true")
}
})
t.Run("msg", func(t *testing.T) {
if got, ok := container.GetErrorMessage(tc.err); !ok {
if tc.msg != "" {
t.Errorf("GetErrorMessage: err does not implement MessageError")
}
return
} else if got != tc.msg {
t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg)
}
})
})
}
}
const ( const (
ignore = "\x00" ignore = "\x00"
ignoreV = -1 ignoreV = -1
@ -214,7 +75,7 @@ var containerTestCases = []struct {
1000, 100, nil, 0, seccomp.PresetExt}, 1000, 100, nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true, false, {"custom rules", true, true, true, false,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1, 31, []seccomp.NativeRule{{Syscall: seccomp.ScmpSyscall(syscall.SYS_SETUID), Errno: seccomp.ScmpErrno(syscall.EPERM)}}, 0, seccomp.PresetExt}, 1, 31, []seccomp.NativeRule{{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}}, 0, seccomp.PresetExt},
{"tmpfs", true, false, false, true, {"tmpfs", true, false, false, true,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
@ -239,7 +100,6 @@ var containerTestCases = []struct {
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
), ),
1971, 100, nil, 0, seccomp.PresetStrict}, 1971, 100, nil, 0, seccomp.PresetStrict},
@ -256,7 +116,6 @@ var containerTestCases = []struct {
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
), ),
1971, 100, nil, 0, seccomp.PresetStrict}, 1971, 100, nil, 0, seccomp.PresetStrict},
@ -356,11 +215,9 @@ func TestContainer(t *testing.T) {
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) { t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled wantErr := context.Canceled
wantExitCode := 0 wantExitCode := 0
if err := c.Wait(); !reflect.DeepEqual(err, wantErr) { if err := c.Wait(); !errors.Is(err, wantErr) {
if m, ok := container.InternalMessageFromError(err); ok { container.GetOutput().PrintBaseErr(err, "wait:")
t.Error(m) t.Errorf("Wait: error = %v, want %v", err, wantErr)
}
t.Errorf("Wait: error = %#v, want %#v", err, wantErr)
} }
if ps := c.ProcessState(); ps == nil { if ps := c.ProcessState(); ps == nil {
t.Errorf("ProcessState unexpectedly returned nil") t.Errorf("ProcessState unexpectedly returned nil")
@ -374,9 +231,7 @@ func TestContainer(t *testing.T) {
}, func(t *testing.T, c *container.Container) { }, func(t *testing.T, c *container.Container) {
var exitError *exec.ExitError var exitError *exec.ExitError
if err := c.Wait(); !errors.As(err, &exitError) { if err := c.Wait(); !errors.As(err, &exitError) {
if m, ok := container.InternalMessageFromError(err); ok { container.GetOutput().PrintBaseErr(err, "wait:")
t.Error(m)
}
t.Errorf("Wait: error = %v", err) t.Errorf("Wait: error = %v", err)
} }
if code := exitError.ExitCode(); code != blockExitCodeInterrupt { if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
@ -456,26 +311,17 @@ func TestContainer(t *testing.T) {
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
_, _ = output.WriteTo(os.Stdout) _, _ = output.WriteTo(os.Stdout)
if m, ok := container.InternalMessageFromError(err); ok { container.GetOutput().PrintBaseErr(err, "start:")
t.Fatal(m) t.Fatalf("cannot start container: %v", err)
} else {
t.Fatalf("cannot start container: %v", err)
}
} else if err = c.Serve(); err != nil { } else if err = c.Serve(); err != nil {
_, _ = output.WriteTo(os.Stdout) _, _ = output.WriteTo(os.Stdout)
if m, ok := container.InternalMessageFromError(err); ok { container.GetOutput().PrintBaseErr(err, "serve:")
t.Error(m) t.Errorf("cannot serve setup params: %v", err)
} else {
t.Errorf("cannot serve setup params: %v", err)
}
} }
if err := c.Wait(); err != nil { if err := c.Wait(); err != nil {
_, _ = output.WriteTo(os.Stdout) _, _ = output.WriteTo(os.Stdout)
if m, ok := container.InternalMessageFromError(err); ok { container.GetOutput().PrintBaseErr(err, "wait:")
t.Fatal(m) t.Fatalf("wait: %v", err)
} else {
t.Fatalf("wait: %v", err)
}
} }
}) })
} }
@ -528,17 +374,11 @@ func testContainerCancel(
} }
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
if m, ok := container.InternalMessageFromError(err); ok { container.GetOutput().PrintBaseErr(err, "start:")
t.Fatal(m) t.Fatalf("cannot start container: %v", err)
} else {
t.Fatalf("cannot start container: %v", err)
}
} else if err = c.Serve(); err != nil { } else if err = c.Serve(); err != nil {
if m, ok := container.InternalMessageFromError(err); ok { container.GetOutput().PrintBaseErr(err, "serve:")
t.Error(m) t.Errorf("cannot serve setup params: %v", err)
} else {
t.Errorf("cannot serve setup params: %v", err)
}
} }
<-ready <-ready
cancel() cancel()

View File

@ -53,7 +53,7 @@ type syscallDispatcher interface {
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// bindMount provides procPaths.bindMount. // bindMount provides procPaths.bindMount.
bindMount(source, target string, flags uintptr) error bindMount(source, target string, flags uintptr, eq bool) error
// remount provides procPaths.remount. // remount provides procPaths.remount.
remount(target string, flags uintptr) error remount(target string, flags uintptr) error
// mountTmpfs provides mountTmpfs. // mountTmpfs provides mountTmpfs.
@ -138,6 +138,8 @@ type syscallDispatcher interface {
resume() bool resume() bool
// beforeExit provides [Msg.BeforeExit]. // beforeExit provides [Msg.BeforeExit].
beforeExit() beforeExit()
// printBaseErr provides [Msg.PrintBaseErr].
printBaseErr(err error, fallback string)
} }
// direct implements syscallDispatcher on the current kernel. // direct implements syscallDispatcher on the current kernel.
@ -161,8 +163,8 @@ func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return Receive(key, e, fdp) return Receive(key, e, fdp)
} }
func (direct) bindMount(source, target string, flags uintptr) error { func (direct) bindMount(source, target string, flags uintptr, eq bool) error {
return hostProc.bindMount(source, target, flags) return hostProc.bindMount(source, target, flags, eq)
} }
func (direct) remount(target string, flags uintptr) error { func (direct) remount(target string, flags uintptr) error {
return hostProc.remount(target, flags) return hostProc.remount(target, flags)
@ -223,7 +225,7 @@ func (direct) pivotRoot(newroot, putold string) (err error) {
return syscall.PivotRoot(newroot, putold) return syscall.PivotRoot(newroot, putold)
} }
func (direct) mount(source, target, fstype string, flags uintptr, data string) (err error) { func (direct) mount(source, target, fstype string, flags uintptr, data string) (err error) {
return mount(source, target, fstype, flags, data) return syscall.Mount(source, target, fstype, flags, data)
} }
func (direct) unmount(target string, flags int) (err error) { func (direct) unmount(target string, flags int) (err error) {
return syscall.Unmount(target, flags) return syscall.Unmount(target, flags)
@ -232,11 +234,12 @@ func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *s
return syscall.Wait4(pid, wstatus, options, rusage) return syscall.Wait4(pid, wstatus, options, rusage)
} }
func (direct) printf(format string, v ...any) { log.Printf(format, v...) } func (direct) printf(format string, v ...any) { log.Printf(format, v...) }
func (direct) fatal(v ...any) { log.Fatal(v...) } func (direct) fatal(v ...any) { log.Fatal(v...) }
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) } func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }
func (direct) verbose(v ...any) { msg.Verbose(v...) } func (direct) verbose(v ...any) { msg.Verbose(v...) }
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) } func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
func (direct) suspend() { msg.Suspend() } func (direct) suspend() { msg.Suspend() }
func (direct) resume() bool { return msg.Resume() } func (direct) resume() bool { return msg.Resume() }
func (direct) beforeExit() { msg.BeforeExit() } func (direct) beforeExit() { msg.BeforeExit() }
func (direct) printBaseErr(err error, fallback string) { msg.PrintBaseErr(err, fallback) }

View File

@ -2,6 +2,7 @@ package container
import ( import (
"bytes" "bytes"
"errors"
"io" "io"
"io/fs" "io/fs"
"os" "os"
@ -10,14 +11,16 @@ import (
"runtime" "runtime"
"slices" "slices"
"strings" "strings"
"sync"
"syscall" "syscall"
"testing" "testing"
"time" "time"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/stub"
) )
var errUnique = errors.New("unique error injected by the test suite")
type opValidTestCase struct { type opValidTestCase struct {
name string name string
op Op op Op
@ -25,15 +28,9 @@ type opValidTestCase struct {
} }
func checkOpsValid(t *testing.T, testCases []opValidTestCase) { func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
t.Helper()
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
t.Helper()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper()
if got := tc.op.Valid(); got != tc.want { if got := tc.op.Valid(); got != tc.want {
t.Errorf("Valid: %v, want %v", got, tc.want) t.Errorf("Valid: %v, want %v", got, tc.want)
} }
@ -49,15 +46,9 @@ type opsBuilderTestCase struct {
} }
func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) { func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
t.Helper()
t.Run("build", func(t *testing.T) { t.Run("build", func(t *testing.T) {
t.Helper()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper()
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) { 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) t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
} }
@ -73,15 +64,9 @@ type opIsTestCase struct {
} }
func checkOpIs(t *testing.T, testCases []opIsTestCase) { func checkOpIs(t *testing.T, testCases []opIsTestCase) {
t.Helper()
t.Run("is", func(t *testing.T) { t.Run("is", func(t *testing.T) {
t.Helper()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper()
if got := tc.op.Is(tc.v); got != tc.want { if got := tc.op.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want) t.Errorf("Is: %v, want %v", got, tc.want)
} }
@ -99,26 +84,16 @@ type opMetaTestCase struct {
} }
func checkOpMeta(t *testing.T, testCases []opMetaTestCase) { func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
t.Helper()
t.Run("meta", func(t *testing.T) { t.Run("meta", func(t *testing.T) {
t.Helper()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Run("prefix", func(t *testing.T) { t.Run("prefix", func(t *testing.T) {
t.Helper() if got := tc.op.prefix(); got != tc.wantPrefix {
if got, _ := tc.op.prefix(); got != tc.wantPrefix {
t.Errorf("prefix: %q, want %q", got, tc.wantPrefix) t.Errorf("prefix: %q, want %q", got, tc.wantPrefix)
} }
}) })
t.Run("string", func(t *testing.T) { t.Run("string", func(t *testing.T) {
t.Helper()
if got := tc.op.String(); got != tc.wantString { if got := tc.op.String(); got != tc.wantString {
t.Errorf("String: %s, want %s", got, tc.wantString) t.Errorf("String: %s, want %s", got, tc.wantString)
} }
@ -128,36 +103,23 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
}) })
} }
// 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}
}
type simpleTestCase struct { type simpleTestCase struct {
name string name string
f func(k syscallDispatcher) error f func(k syscallDispatcher) error
want stub.Expect want [][]kexpect
wantErr error wantErr error
} }
func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) { func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
t.Helper()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() defer handleExitStub()
k := &kstub{t: t, want: tc.want, wg: new(sync.WaitGroup)}
wait4signal := make(chan struct{}) if err := tc.f(k); !errors.Is(err, tc.wantErr) {
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
defer stub.HandleExit(t)
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr) t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
} }
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) { k.handleIncomplete(func(k *kstub) {
t.Helper() t.Errorf("%s: %d calls, want %d (track %d)", fname, k.pos, len(k.want[k.track]), k.track)
t.Errorf("%s: %d calls, want %d", fname, s.Pos(), s.Len())
}) })
}) })
} }
@ -168,45 +130,36 @@ type opBehaviourTestCase struct {
params *Params params *Params
op Op op Op
early []stub.Call early []kexpect
wantErrEarly error wantErrEarly error
apply []stub.Call apply []kexpect
wantErrApply error wantErrApply error
} }
func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Helper()
t.Run("behaviour", func(t *testing.T) { t.Run("behaviour", func(t *testing.T) {
t.Helper()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() defer handleExitStub()
state := &setupState{Params: tc.params} state := &setupState{Params: tc.params}
k := &kstub{nil, stub.New(t, k := &kstub{t: t, want: [][]kexpect{slices.Concat(tc.early, []kexpect{{name: "\x00"}}, tc.apply)}, wg: new(sync.WaitGroup)}
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
)}
defer stub.HandleExit(t)
errEarly := tc.op.early(state, k) errEarly := tc.op.early(state, k)
k.Expects(stub.CallSeparator) k.expect("\x00")
if !reflect.DeepEqual(errEarly, tc.wantErrEarly) { if !errors.Is(errEarly, tc.wantErrEarly) {
t.Errorf("early: error = %v, want %v", errEarly, tc.wantErrEarly) t.Errorf("early: error = %v, want %v", errEarly, tc.wantErrEarly)
} }
if errEarly != nil { if errEarly != nil {
goto out goto out
} }
if err := tc.op.apply(state, k); !reflect.DeepEqual(err, tc.wantErrApply) { if err := tc.op.apply(state, k); !errors.Is(err, tc.wantErrApply) {
t.Errorf("apply: error = %v, want %v", err, tc.wantErrApply) t.Errorf("apply: error = %v, want %v", err, tc.wantErrApply)
} }
out: out:
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) { k.handleIncomplete(func(k *kstub) {
count := k.Pos() - 1 // separator count := k.pos - 1 // separator
if count < len(tc.early) { if count < len(tc.early) {
t.Errorf("early: %d calls, want %d", count, len(tc.early)) t.Errorf("early: %d calls, want %d", count, len(tc.early))
} else { } else {
@ -273,6 +226,8 @@ func (writeErrOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
func (writeErrOsFile) Read([]byte) (int, error) { panic("unreachable") } func (writeErrOsFile) Read([]byte) (int, error) { panic("unreachable") }
func (writeErrOsFile) Close() error { panic("unreachable") } func (writeErrOsFile) Close() error { panic("unreachable") }
type expectArgs = [5]any
type isDirFi bool type isDirFi bool
func (isDirFi) Name() string { panic("unreachable") } func (isDirFi) Name() string { panic("unreachable") }
@ -297,94 +252,184 @@ func (nameDentry) IsDir() bool { panic("unreachable") }
func (nameDentry) Type() fs.FileMode { panic("unreachable") } func (nameDentry) Type() fs.FileMode { panic("unreachable") }
func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") } func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") }
const ( type kexpect struct {
// magicWait4Signal must be used in a single pair of signal and wait4 calls across two goroutines name string
// originating from the same toplevel kstub. args expectArgs
// To enable this behaviour this value must be the last element of the args field in the wait4 call ret any
// and the ret value of the signal call. err error
magicWait4Signal = 0xdef
)
type kstub struct {
wait4signal chan struct{}
*stub.Stub[syscallDispatcher]
} }
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) } func (k *kexpect) error(ok ...bool) error {
if !slices.Contains(ok, false) {
return k.err
}
return syscall.ENOTRECOVERABLE
}
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") } func handleExitStub() {
r := recover()
if r == 0xdeadbeef {
return
}
if r != nil {
panic(r)
}
}
type kstub struct {
t *testing.T
want [][]kexpect
// pos is the current position in want[track].
pos int
// track is the current active want.
track int
// sub stores addresses of kstub created by new.
sub []*kstub
// wg waits for all descendants to complete.
wg *sync.WaitGroup
}
// handleIncomplete calls f on an incomplete k and all its descendants.
func (k *kstub) handleIncomplete(f func(k *kstub)) {
k.wg.Wait()
if k.want != nil && len(k.want[k.track]) != k.pos {
f(k)
}
for _, sk := range k.sub {
sk.handleIncomplete(f)
}
}
// expect checks name and returns the current kexpect and advances pos.
func (k *kstub) expect(name string) (expect *kexpect) {
if len(k.want[k.track]) == k.pos {
k.t.Fatal("expect: want too short")
}
expect = &k.want[k.track][k.pos]
if name != expect.name {
if expect.name == "\x00" {
k.t.Fatalf("expect: func = %s, separator overrun", name)
}
if name == "\x00" {
k.t.Fatalf("expect: separator, want %s", expect.name)
}
k.t.Fatalf("expect: func = %s, want %s", name, expect.name)
}
k.pos++
return
}
// checkArg checks an argument comparable with the == operator. Avoid using this with pointers.
func checkArg[T comparable](k *kstub, arg string, got T, n int) bool {
if k.pos == 0 {
panic("invalid call to checkArg")
}
expect := k.want[k.track][k.pos-1]
want, ok := expect.args[n].(T)
if !ok || got != want {
k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1)
return false
}
return true
}
// checkArgReflect checks an argument of any type.
func checkArgReflect(k *kstub, arg string, got any, n int) bool {
if k.pos == 0 {
panic("invalid call to checkArgReflect")
}
expect := k.want[k.track][k.pos-1]
want := expect.args[n]
if !reflect.DeepEqual(got, want) {
k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1)
return false
}
return true
}
func (k *kstub) new(f func(k syscallDispatcher)) {
k.expect("new")
if len(k.want) <= k.track+1 {
k.t.Fatalf("new: track overrun")
}
sk := &kstub{t: k.t, want: k.want, track: len(k.sub) + 1, wg: k.wg}
k.sub = append(k.sub, sk)
k.wg.Add(1)
go func() {
defer k.wg.Done()
defer handleExitStub()
f(sk)
}()
}
func (k *kstub) lockOSThread() { k.expect("lockOSThread") }
func (k *kstub) setPtracer(pid uintptr) error { func (k *kstub) setPtracer(pid uintptr) error {
k.Helper() return k.expect("setPtracer").error(
return k.Expects("setPtracer").Error( checkArg(k, "pid", pid, 0))
stub.CheckArg(k.Stub, "pid", pid, 0))
} }
func (k *kstub) setDumpable(dumpable uintptr) error { func (k *kstub) setDumpable(dumpable uintptr) error {
k.Helper() return k.expect("setDumpable").error(
return k.Expects("setDumpable").Error( checkArg(k, "dumpable", dumpable, 0))
stub.CheckArg(k.Stub, "dumpable", dumpable, 0))
} }
func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err } func (k *kstub) setNoNewPrivs() error { return k.expect("setNoNewPrivs").err }
func (k *kstub) lastcap() uintptr { k.Helper(); return k.Expects("lastcap").Ret.(uintptr) } func (k *kstub) lastcap() uintptr { return k.expect("lastcap").ret.(uintptr) }
func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error { func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
k.Helper() return k.expect("capset").error(
return k.Expects("capset").Error( checkArgReflect(k, "hdrp", hdrp, 0),
stub.CheckArgReflect(k.Stub, "hdrp", hdrp, 0), checkArgReflect(k, "datap", datap, 1))
stub.CheckArgReflect(k.Stub, "datap", datap, 1))
} }
func (k *kstub) capBoundingSetDrop(cap uintptr) error { func (k *kstub) capBoundingSetDrop(cap uintptr) error {
k.Helper() return k.expect("capBoundingSetDrop").error(
return k.Expects("capBoundingSetDrop").Error( checkArg(k, "cap", cap, 0))
stub.CheckArg(k.Stub, "cap", cap, 0))
} }
func (k *kstub) capAmbientClearAll() error { k.Helper(); return k.Expects("capAmbientClearAll").Err } func (k *kstub) capAmbientClearAll() error { return k.expect("capAmbientClearAll").err }
func (k *kstub) capAmbientRaise(cap uintptr) error { func (k *kstub) capAmbientRaise(cap uintptr) error {
k.Helper() return k.expect("capAmbientRaise").error(
return k.Expects("capAmbientRaise").Error( checkArg(k, "cap", cap, 0))
stub.CheckArg(k.Stub, "cap", cap, 0))
} }
func (k *kstub) isatty(fd int) bool { func (k *kstub) isatty(fd int) bool {
k.Helper() expect := k.expect("isatty")
expect := k.Expects("isatty") if !checkArg(k, "fd", fd, 0) {
if !stub.CheckArg(k.Stub, "fd", fd, 0) { k.t.FailNow()
k.FailNow()
} }
return expect.Ret.(bool) return expect.ret.(bool)
} }
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) { func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
k.Helper() expect := k.expect("receive")
expect := k.Expects("receive")
var closed bool var closed bool
closeFunc = func() error { closeFunc = func() error {
if closed { if closed {
k.Error("closeFunc called more than once") k.t.Error("closeFunc called more than once")
return os.ErrClosed return os.ErrClosed
} }
closed = true closed = true
if expect.Ret != nil { if expect.ret != nil {
// use return stored in kexpect for closeFunc instead // use return stored in kexpect for closeFunc instead
return expect.Ret.(error) return expect.ret.(error)
} }
return nil return nil
} }
err = expect.Error( err = expect.error(
stub.CheckArg(k.Stub, "key", key, 0), checkArg(k, "key", key, 0),
stub.CheckArgReflect(k.Stub, "e", e, 1), checkArgReflect(k, "e", e, 1),
stub.CheckArgReflect(k.Stub, "fdp", fdp, 2)) checkArgReflect(k, "fdp", fdp, 2))
// 3 is unused so stores params // 3 is unused so stores params
if expect.Args[3] != nil { if expect.args[3] != nil {
if v, ok := expect.Args[3].(*initParams); ok && v != nil { if v, ok := expect.args[3].(*initParams); ok && v != nil {
if p, ok0 := e.(*initParams); ok0 && p != nil { if p, ok0 := e.(*initParams); ok0 && p != nil {
*p = *v *p = *v
} }
@ -392,8 +437,8 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
} }
// 4 is unused so stores fd // 4 is unused so stores fd
if expect.Args[4] != nil { if expect.args[4] != nil {
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 { if v, ok := expect.args[4].(uintptr); ok && v >= 3 {
if fdp != nil { if fdp != nil {
*fdp = v *fdp = v
} }
@ -403,291 +448,247 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
return return
} }
func (k *kstub) bindMount(source, target string, flags uintptr) error { func (k *kstub) bindMount(source, target string, flags uintptr, eq bool) error {
k.Helper() return k.expect("bindMount").error(
return k.Expects("bindMount").Error( checkArg(k, "source", source, 0),
stub.CheckArg(k.Stub, "source", source, 0), checkArg(k, "target", target, 1),
stub.CheckArg(k.Stub, "target", target, 1), checkArg(k, "flags", flags, 2),
stub.CheckArg(k.Stub, "flags", flags, 2)) checkArg(k, "eq", eq, 3))
} }
func (k *kstub) remount(target string, flags uintptr) error { func (k *kstub) remount(target string, flags uintptr) error {
k.Helper() return k.expect("remount").error(
return k.Expects("remount").Error( checkArg(k, "target", target, 0),
stub.CheckArg(k.Stub, "target", target, 0), checkArg(k, "flags", flags, 1))
stub.CheckArg(k.Stub, "flags", flags, 1))
} }
func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error { func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
k.Helper() return k.expect("mountTmpfs").error(
return k.Expects("mountTmpfs").Error( checkArg(k, "fsname", fsname, 0),
stub.CheckArg(k.Stub, "fsname", fsname, 0), checkArg(k, "target", target, 1),
stub.CheckArg(k.Stub, "target", target, 1), checkArg(k, "flags", flags, 2),
stub.CheckArg(k.Stub, "flags", flags, 2), checkArg(k, "size", size, 3),
stub.CheckArg(k.Stub, "size", size, 3), checkArg(k, "perm", perm, 4))
stub.CheckArg(k.Stub, "perm", perm, 4))
} }
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error { func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
k.Helper()
return k.Expects("ensureFile").Error( return k.expect("ensureFile").error(
stub.CheckArg(k.Stub, "name", name, 0), checkArg(k, "name", name, 0),
stub.CheckArg(k.Stub, "perm", perm, 1), checkArg(k, "perm", perm, 1),
stub.CheckArg(k.Stub, "pperm", pperm, 2)) checkArg(k, "pperm", pperm, 2))
} }
func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error { func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
k.Helper() return k.expect("seccompLoad").error(
return k.Expects("seccompLoad").Error( checkArgReflect(k, "rules", rules, 0),
stub.CheckArgReflect(k.Stub, "rules", rules, 0), checkArg(k, "flags", flags, 1))
stub.CheckArg(k.Stub, "flags", flags, 1))
} }
func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) { func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
k.Helper() expect := k.expect("notify")
expect := k.Expects("notify") if c == nil || expect.error(
if c == nil || expect.Error( checkArgReflect(k, "sig", sig, 1)) != nil {
stub.CheckArgReflect(k.Stub, "sig", sig, 1)) != nil { k.t.FailNow()
k.FailNow()
} }
// export channel for external instrumentation // export channel for external instrumentation
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil { if chanf, ok := expect.args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
chanf(c) chanf(c)
} }
} }
func (k *kstub) start(c *exec.Cmd) error { func (k *kstub) start(c *exec.Cmd) error {
k.Helper() expect := k.expect("start")
expect := k.Expects("start") err := expect.error(
err := expect.Error( checkArg(k, "c.Path", c.Path, 0),
stub.CheckArg(k.Stub, "c.Path", c.Path, 0), checkArgReflect(k, "c.Args", c.Args, 1),
stub.CheckArgReflect(k.Stub, "c.Args", c.Args, 1), checkArgReflect(k, "c.Env", c.Env, 2),
stub.CheckArgReflect(k.Stub, "c.Env", c.Env, 2), checkArg(k, "c.Dir", c.Dir, 3))
stub.CheckArg(k.Stub, "c.Dir", c.Dir, 3))
if process, ok := expect.Ret.(*os.Process); ok && process != nil { if process, ok := expect.ret.(*os.Process); ok && process != nil {
c.Process = process c.Process = process
} }
return err return err
} }
func (k *kstub) signal(c *exec.Cmd, sig os.Signal) error { func (k *kstub) signal(c *exec.Cmd, sig os.Signal) error {
k.Helper() return k.expect("signal").error(
expect := k.Expects("signal") checkArg(k, "c.Path", c.Path, 0),
if v, ok := expect.Ret.(int); ok && v == magicWait4Signal { checkArgReflect(k, "c.Args", c.Args, 1),
if k.wait4signal == nil { checkArgReflect(k, "c.Env", c.Env, 2),
panic("kstub not initialised for wait4 simulation") checkArg(k, "c.Dir", c.Dir, 3),
} checkArg(k, "sig", sig, 4))
defer func() { close(k.wait4signal) }()
}
return expect.Error(
stub.CheckArg(k.Stub, "c.Path", c.Path, 0),
stub.CheckArgReflect(k.Stub, "c.Args", c.Args, 1),
stub.CheckArgReflect(k.Stub, "c.Env", c.Env, 2),
stub.CheckArg(k.Stub, "c.Dir", c.Dir, 3),
stub.CheckArg(k.Stub, "sig", sig, 4))
} }
func (k *kstub) evalSymlinks(path string) (string, error) { func (k *kstub) evalSymlinks(path string) (string, error) {
k.Helper() expect := k.expect("evalSymlinks")
expect := k.Expects("evalSymlinks") return expect.ret.(string), expect.error(
return expect.Ret.(string), expect.Error( checkArg(k, "path", path, 0))
stub.CheckArg(k.Stub, "path", path, 0))
} }
func (k *kstub) exit(code int) { func (k *kstub) exit(code int) {
k.Helper() k.expect("exit")
k.Expects("exit") if !checkArg(k, "code", code, 0) {
if !stub.CheckArg(k.Stub, "code", code, 0) { k.t.FailNow()
k.FailNow()
} }
panic(stub.PanicExit) panic(0xdeadbeef)
} }
func (k *kstub) getpid() int { k.Helper(); return k.Expects("getpid").Ret.(int) } func (k *kstub) getpid() int { return k.expect("getpid").ret.(int) }
func (k *kstub) stat(name string) (os.FileInfo, error) { func (k *kstub) stat(name string) (os.FileInfo, error) {
k.Helper() expect := k.expect("stat")
expect := k.Expects("stat") return expect.ret.(os.FileInfo), expect.error(
return expect.Ret.(os.FileInfo), expect.Error( checkArg(k, "name", name, 0))
stub.CheckArg(k.Stub, "name", name, 0))
} }
func (k *kstub) mkdir(name string, perm os.FileMode) error { func (k *kstub) mkdir(name string, perm os.FileMode) error {
k.Helper() return k.expect("mkdir").error(
return k.Expects("mkdir").Error( checkArg(k, "name", name, 0),
stub.CheckArg(k.Stub, "name", name, 0), checkArg(k, "perm", perm, 1))
stub.CheckArg(k.Stub, "perm", perm, 1))
} }
func (k *kstub) mkdirTemp(dir, pattern string) (string, error) { func (k *kstub) mkdirTemp(dir, pattern string) (string, error) {
k.Helper() expect := k.expect("mkdirTemp")
expect := k.Expects("mkdirTemp") return expect.ret.(string), expect.error(
return expect.Ret.(string), expect.Error( checkArg(k, "dir", dir, 0),
stub.CheckArg(k.Stub, "dir", dir, 0), checkArg(k, "pattern", pattern, 1))
stub.CheckArg(k.Stub, "pattern", pattern, 1))
} }
func (k *kstub) mkdirAll(path string, perm os.FileMode) error { func (k *kstub) mkdirAll(path string, perm os.FileMode) error {
k.Helper() return k.expect("mkdirAll").error(
return k.Expects("mkdirAll").Error( checkArg(k, "path", path, 0),
stub.CheckArg(k.Stub, "path", path, 0), checkArg(k, "perm", perm, 1))
stub.CheckArg(k.Stub, "perm", perm, 1))
} }
func (k *kstub) readdir(name string) ([]os.DirEntry, error) { func (k *kstub) readdir(name string) ([]os.DirEntry, error) {
k.Helper() expect := k.expect("readdir")
expect := k.Expects("readdir") return expect.ret.([]os.DirEntry), expect.error(
return expect.Ret.([]os.DirEntry), expect.Error( checkArg(k, "name", name, 0))
stub.CheckArg(k.Stub, "name", name, 0))
} }
func (k *kstub) openNew(name string) (osFile, error) { func (k *kstub) openNew(name string) (osFile, error) {
k.Helper() expect := k.expect("openNew")
expect := k.Expects("openNew") return expect.ret.(osFile), expect.error(
return expect.Ret.(osFile), expect.Error( checkArg(k, "name", name, 0))
stub.CheckArg(k.Stub, "name", name, 0))
} }
func (k *kstub) writeFile(name string, data []byte, perm os.FileMode) error { func (k *kstub) writeFile(name string, data []byte, perm os.FileMode) error {
k.Helper() return k.expect("writeFile").error(
return k.Expects("writeFile").Error( checkArg(k, "name", name, 0),
stub.CheckArg(k.Stub, "name", name, 0), checkArgReflect(k, "data", data, 1),
stub.CheckArgReflect(k.Stub, "data", data, 1), checkArg(k, "perm", perm, 2))
stub.CheckArg(k.Stub, "perm", perm, 2))
} }
func (k *kstub) createTemp(dir, pattern string) (osFile, error) { func (k *kstub) createTemp(dir, pattern string) (osFile, error) {
k.Helper() expect := k.expect("createTemp")
expect := k.Expects("createTemp") return expect.ret.(osFile), expect.error(
return expect.Ret.(osFile), expect.Error( checkArg(k, "dir", dir, 0),
stub.CheckArg(k.Stub, "dir", dir, 0), checkArg(k, "pattern", pattern, 1))
stub.CheckArg(k.Stub, "pattern", pattern, 1))
} }
func (k *kstub) remove(name string) error { func (k *kstub) remove(name string) error {
k.Helper() return k.expect("remove").error(
return k.Expects("remove").Error( checkArg(k, "name", name, 0))
stub.CheckArg(k.Stub, "name", name, 0))
} }
func (k *kstub) newFile(fd uintptr, name string) *os.File { func (k *kstub) newFile(fd uintptr, name string) *os.File {
k.Helper() expect := k.expect("newFile")
expect := k.Expects("newFile") if expect.error(
if expect.Error( checkArg(k, "fd", fd, 0),
stub.CheckArg(k.Stub, "fd", fd, 0), checkArg(k, "name", name, 1)) != nil {
stub.CheckArg(k.Stub, "name", name, 1)) != nil { k.t.FailNow()
k.FailNow()
} }
return expect.Ret.(*os.File) return expect.ret.(*os.File)
} }
func (k *kstub) symlink(oldname, newname string) error { func (k *kstub) symlink(oldname, newname string) error {
k.Helper() return k.expect("symlink").error(
return k.Expects("symlink").Error( checkArg(k, "oldname", oldname, 0),
stub.CheckArg(k.Stub, "oldname", oldname, 0), checkArg(k, "newname", newname, 1))
stub.CheckArg(k.Stub, "newname", newname, 1))
} }
func (k *kstub) readlink(name string) (string, error) { func (k *kstub) readlink(name string) (string, error) {
k.Helper() expect := k.expect("readlink")
expect := k.Expects("readlink") return expect.ret.(string), expect.error(
return expect.Ret.(string), expect.Error( checkArg(k, "name", name, 0))
stub.CheckArg(k.Stub, "name", name, 0))
} }
func (k *kstub) umask(mask int) (oldmask int) { func (k *kstub) umask(mask int) (oldmask int) {
k.Helper() expect := k.expect("umask")
expect := k.Expects("umask") if !checkArg(k, "mask", mask, 0) {
if !stub.CheckArg(k.Stub, "mask", mask, 0) { k.t.FailNow()
k.FailNow()
} }
return expect.Ret.(int) return expect.ret.(int)
} }
func (k *kstub) sethostname(p []byte) (err error) { func (k *kstub) sethostname(p []byte) (err error) {
k.Helper() return k.expect("sethostname").error(
return k.Expects("sethostname").Error( checkArgReflect(k, "p", p, 0))
stub.CheckArgReflect(k.Stub, "p", p, 0))
} }
func (k *kstub) chdir(path string) (err error) { func (k *kstub) chdir(path string) (err error) {
k.Helper() return k.expect("chdir").error(
return k.Expects("chdir").Error( checkArg(k, "path", path, 0))
stub.CheckArg(k.Stub, "path", path, 0))
} }
func (k *kstub) fchdir(fd int) (err error) { func (k *kstub) fchdir(fd int) (err error) {
k.Helper() return k.expect("fchdir").error(
return k.Expects("fchdir").Error( checkArg(k, "fd", fd, 0))
stub.CheckArg(k.Stub, "fd", fd, 0))
} }
func (k *kstub) open(path string, mode int, perm uint32) (fd int, err error) { func (k *kstub) open(path string, mode int, perm uint32) (fd int, err error) {
k.Helper() expect := k.expect("open")
expect := k.Expects("open") return expect.ret.(int), expect.error(
return expect.Ret.(int), expect.Error( checkArg(k, "path", path, 0),
stub.CheckArg(k.Stub, "path", path, 0), checkArg(k, "mode", mode, 1),
stub.CheckArg(k.Stub, "mode", mode, 1), checkArg(k, "perm", perm, 2))
stub.CheckArg(k.Stub, "perm", perm, 2))
} }
func (k *kstub) close(fd int) (err error) { func (k *kstub) close(fd int) (err error) {
k.Helper() return k.expect("close").error(
return k.Expects("close").Error( checkArg(k, "fd", fd, 0))
stub.CheckArg(k.Stub, "fd", fd, 0))
} }
func (k *kstub) pivotRoot(newroot, putold string) (err error) { func (k *kstub) pivotRoot(newroot, putold string) (err error) {
k.Helper() return k.expect("pivotRoot").error(
return k.Expects("pivotRoot").Error( checkArg(k, "newroot", newroot, 0),
stub.CheckArg(k.Stub, "newroot", newroot, 0), checkArg(k, "putold", putold, 1))
stub.CheckArg(k.Stub, "putold", putold, 1))
} }
func (k *kstub) mount(source, target, fstype string, flags uintptr, data string) (err error) { func (k *kstub) mount(source, target, fstype string, flags uintptr, data string) (err error) {
k.Helper() return k.expect("mount").error(
return k.Expects("mount").Error( checkArg(k, "source", source, 0),
stub.CheckArg(k.Stub, "source", source, 0), checkArg(k, "target", target, 1),
stub.CheckArg(k.Stub, "target", target, 1), checkArg(k, "fstype", fstype, 2),
stub.CheckArg(k.Stub, "fstype", fstype, 2), checkArg(k, "flags", flags, 3),
stub.CheckArg(k.Stub, "flags", flags, 3), checkArg(k, "data", data, 4))
stub.CheckArg(k.Stub, "data", data, 4))
} }
func (k *kstub) unmount(target string, flags int) (err error) { func (k *kstub) unmount(target string, flags int) (err error) {
k.Helper() return k.expect("unmount").error(
return k.Expects("unmount").Error( checkArg(k, "target", target, 0),
stub.CheckArg(k.Stub, "target", target, 0), checkArg(k, "flags", flags, 1))
stub.CheckArg(k.Stub, "flags", flags, 1))
} }
func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) { func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) {
k.Helper() expect := k.expect("wait4")
expect := k.Expects("wait4") // special case to prevent leaking the wait4 goroutine when testing initEntrypoint
if v, ok := expect.Args[4].(int); ok { if v, ok := expect.args[4].(int); ok && v == 0xdeadbeef {
switch v { k.t.Log("terminating current goroutine as requested by kexpect")
case stub.PanicExit: // special case to prevent leaking the wait4 goroutine while testing initEntrypoint panic(0xdeadbeef)
panic(stub.PanicExit)
case magicWait4Signal: // block until corresponding signal call
if k.wait4signal == nil {
panic("kstub not initialised for wait4 simulation")
}
<-k.wait4signal
}
} }
wpid = expect.Ret.(int) wpid = expect.ret.(int)
err = expect.Error( err = expect.error(
stub.CheckArg(k.Stub, "pid", pid, 0), checkArg(k, "pid", pid, 0),
stub.CheckArg(k.Stub, "options", options, 2)) checkArg(k, "options", options, 2))
if wstatusV, ok := expect.Args[1].(syscall.WaitStatus); wstatus != nil && ok { if wstatusV, ok := expect.args[1].(syscall.WaitStatus); wstatus != nil && ok {
*wstatus = wstatusV *wstatus = wstatusV
} }
if rusageV, ok := expect.Args[3].(syscall.Rusage); rusage != nil && ok { if rusageV, ok := expect.args[3].(syscall.Rusage); rusage != nil && ok {
*rusage = rusageV *rusage = rusageV
} }
@ -695,50 +696,53 @@ func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage
} }
func (k *kstub) printf(format string, v ...any) { func (k *kstub) printf(format string, v ...any) {
k.Helper() if k.expect("printf").error(
if k.Expects("printf").Error( checkArg(k, "format", format, 0),
stub.CheckArg(k.Stub, "format", format, 0), checkArgReflect(k, "v", v, 1)) != nil {
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil { k.t.FailNow()
k.FailNow()
} }
} }
func (k *kstub) fatal(v ...any) { func (k *kstub) fatal(v ...any) {
k.Helper() if k.expect("fatal").error(
if k.Expects("fatal").Error( checkArgReflect(k, "v", v, 0)) != nil {
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { k.t.FailNow()
k.FailNow()
} }
panic(stub.PanicExit) panic(0xdeadbeef)
} }
func (k *kstub) fatalf(format string, v ...any) { func (k *kstub) fatalf(format string, v ...any) {
k.Helper() if k.expect("fatalf").error(
if k.Expects("fatalf").Error( checkArg(k, "format", format, 0),
stub.CheckArg(k.Stub, "format", format, 0), checkArgReflect(k, "v", v, 1)) != nil {
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil { k.t.FailNow()
k.FailNow()
} }
panic(stub.PanicExit) panic(0xdeadbeef)
} }
func (k *kstub) verbose(v ...any) { func (k *kstub) verbose(v ...any) {
k.Helper() if k.expect("verbose").error(
if k.Expects("verbose").Error( checkArgReflect(k, "v", v, 0)) != nil {
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { k.t.FailNow()
k.FailNow()
} }
} }
func (k *kstub) verbosef(format string, v ...any) { func (k *kstub) verbosef(format string, v ...any) {
k.Helper() if k.expect("verbosef").error(
if k.Expects("verbosef").Error( checkArg(k, "format", format, 0),
stub.CheckArg(k.Stub, "format", format, 0), checkArgReflect(k, "v", v, 1)) != nil {
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil { k.t.FailNow()
k.FailNow()
} }
} }
func (k *kstub) suspend() { k.Helper(); k.Expects("suspend") } func (k *kstub) suspend() { k.expect("suspend") }
func (k *kstub) resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) } func (k *kstub) resume() bool { return k.expect("resume").ret.(bool) }
func (k *kstub) beforeExit() { k.Helper(); k.Expects("beforeExit") } func (k *kstub) beforeExit() { k.expect("beforeExit") }
func (k *kstub) printBaseErr(err error, fallback string) {
if k.expect("printBaseErr").error(
checkArgReflect(k, "err", err, 0),
checkArg(k, "fallback", fallback, 1)) != nil {
k.t.FailNow()
}
}

View File

@ -1,112 +0,0 @@
package container
import (
"errors"
"os"
"syscall"
"hakurei.app/container/vfs"
)
// messageFromError returns a printable error message for a supported concrete type.
func messageFromError(err error) (string, bool) {
if m, ok := messagePrefixP[MountError]("cannot ", err); ok {
return m, ok
}
if m, ok := messagePrefixP[os.PathError]("cannot ", err); ok {
return m, ok
}
if m, ok := messagePrefixP[AbsoluteError]("", err); ok {
return m, ok
}
if m, ok := messagePrefix[OpRepeatError]("", err); ok {
return m, ok
}
if m, ok := messagePrefix[OpStateError]("", err); ok {
return m, ok
}
if m, ok := messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
return m, ok
}
if m, ok := messagePrefix[TmpfsSizeError]("", err); ok {
return m, ok
}
return zeroString, false
}
// messagePrefix checks and prefixes the error message of a non-pointer error.
// While this is usable for pointer errors, such use should be avoided as nil check is omitted.
func messagePrefix[T error](prefix string, err error) (string, bool) {
var targetError T
if errors.As(err, &targetError) {
return prefix + targetError.Error(), true
}
return zeroString, false
}
// messagePrefixP checks and prefixes the error message of a pointer error.
func messagePrefixP[V any, T interface {
*V
error
}](prefix string, err error) (string, bool) {
var targetError T
if errors.As(err, &targetError) && targetError != nil {
return prefix + targetError.Error(), true
}
return zeroString, false
}
type MountError struct {
Source, Target, Fstype string
Flags uintptr
Data string
syscall.Errno
}
func (e *MountError) Unwrap() error {
if e.Errno == 0 {
return nil
}
return e.Errno
}
func (e *MountError) Error() string {
if e.Flags&syscall.MS_BIND != 0 {
if e.Flags&syscall.MS_REMOUNT != 0 {
return "remount " + e.Target + ": " + e.Errno.Error()
}
return "bind " + e.Source + " on " + e.Target + ": " + e.Errno.Error()
}
if e.Fstype != FstypeNULL {
return "mount " + e.Fstype + " on " + e.Target + ": " + e.Errno.Error()
}
// fallback case: if this is reached, the conditions for it to occur should be handled above
return "mount " + e.Target + ": " + e.Errno.Error()
}
// 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
if !errors.As(err, &errno) {
return 0, &os.PathError{Op: op, Path: path, Err: err}
}
return errno, nil
}
// mount wraps syscall.Mount for error handling.
func mount(source, target, fstype string, flags uintptr, data string) error {
err := syscall.Mount(source, target, fstype, flags, data)
if err == nil {
return nil
}
if errno, pathError := errnoFallback("mount", target, err); pathError != nil {
return pathError
} else {
return &MountError{source, target, fstype, flags, data, errno}
}
}

View File

@ -1,168 +0,0 @@
package container
import (
"errors"
"os"
"reflect"
"strconv"
"syscall"
"testing"
"hakurei.app/container/stub"
"hakurei.app/container/vfs"
)
func TestMessageFromError(t *testing.T) {
testCases := []struct {
name string
err error
want string
wantOk bool
}{
{"mount", &MountError{
Source: SourceTmpfsEphemeral,
Target: "/sysroot/tmp",
Fstype: FstypeTmpfs,
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Data: zeroString,
Errno: syscall.EINVAL,
}, "cannot mount tmpfs on /sysroot/tmp: invalid argument", true},
{"path", &os.PathError{
Op: "mount",
Path: "/sysroot",
Err: stub.UniqueError(0xdeadbeef),
}, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true},
{"absolute", &AbsoluteError{"etc/mtab"},
`path "etc/mtab" is not absolute`, true},
{"repeat", OpRepeatError("autoetc"),
"autoetc is not repeatable", true},
{"state", OpStateError("overlay"),
"impossible overlay state reached", true},
{"vfs parse", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
`cannot parse mountinfo at line 3735928559: numeric field "meow" invalid syntax`, true},
{"tmpfs", TmpfsSizeError(-1),
"tmpfs size -1 out of bounds", true},
{"unsupported", stub.UniqueError(0xdeadbeef), zeroString, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, ok := messageFromError(tc.err)
if got != tc.want {
t.Errorf("messageFromError: %q, want %q", got, tc.want)
}
if ok != tc.wantOk {
t.Errorf("messageFromError: ok = %v, want %v", ok, tc.wantOk)
}
})
}
}
func TestMountError(t *testing.T) {
testCases := []struct {
name string
err error
errno syscall.Errno
want string
}{
{"bind", &MountError{
Source: "/host/nix/store",
Target: "/sysroot/nix/store",
Fstype: FstypeNULL,
Flags: syscall.MS_SILENT | syscall.MS_BIND | syscall.MS_REC,
Data: zeroString,
Errno: syscall.ENOSYS,
}, syscall.ENOSYS,
"bind /host/nix/store on /sysroot/nix/store: function not implemented"},
{"remount", &MountError{
Source: SourceNone,
Target: "/sysroot/nix/store",
Fstype: FstypeNULL,
Flags: syscall.MS_SILENT | syscall.MS_BIND | syscall.MS_REMOUNT,
Data: zeroString,
Errno: syscall.EPERM,
}, syscall.EPERM,
"remount /sysroot/nix/store: operation not permitted"},
{"overlay", &MountError{
Source: SourceOverlay,
Target: sysrootPath,
Fstype: FstypeOverlay,
Data: `lowerdir=/host/var/lib/planterette/base/debian\:f92c9052`,
Errno: syscall.EINVAL,
}, syscall.EINVAL,
"mount overlay on /sysroot: invalid argument"},
{"fallback", &MountError{
Source: SourceNone,
Target: sysrootPath,
Fstype: FstypeNULL,
Errno: syscall.ENOTRECOVERABLE,
}, syscall.ENOTRECOVERABLE,
"mount /sysroot: state not recoverable"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
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)
}
})
t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want)
}
})
})
}
t.Run("zero", func(t *testing.T) {
if errors.Is(new(MountError), syscall.Errno(0)) {
t.Errorf("Is: zero MountError unexpected true")
}
})
}
func TestErrnoFallback(t *testing.T) {
testCases := []struct {
name string
err error
wantErrno syscall.Errno
wantPath *os.PathError
}{
{"mount", &MountError{
Errno: syscall.ENOTRECOVERABLE,
}, syscall.ENOTRECOVERABLE, nil},
{"path errno", &os.PathError{
Err: syscall.ETIMEDOUT,
}, syscall.ETIMEDOUT, nil},
{"fallback", stub.UniqueError(0xcafebabe), 0, &os.PathError{
Op: "fallback",
Path: "/proc/nonexistent",
Err: stub.UniqueError(0xcafebabe),
}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
errno, err := errnoFallback(tc.name, Nonexistent, tc.err)
if errno != tc.wantErrno {
t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno)
}
if !reflect.DeepEqual(err, tc.wantPath) {
t.Errorf("errnoFallback: pathError = %#v, want %#v", err, tc.wantPath)
}
})
}
}
// InternalMessageFromError exports messageFromError for other tests.
func InternalMessageFromError(err error) (string, bool) { return messageFromError(err) }

View File

@ -47,9 +47,7 @@ type (
// apply is called in intermediate root. // apply is called in intermediate root.
apply(state *setupState, k syscallDispatcher) error apply(state *setupState, k syscallDispatcher) error
// prefix returns a log message prefix, and whether this Op prints no identifying message on its own. prefix() string
prefix() (string, bool)
Is(op Op) bool Is(op Op) bool
Valid() bool Valid() bool
fmt.Stringer fmt.Stringer
@ -70,16 +68,6 @@ const (
nrAutoRoot nrAutoRoot
) )
// OpRepeatError is returned applying a repeated nonrepeatable [Op].
type OpRepeatError string
func (e OpRepeatError) Error() string { return string(e) + " is not repeatable" }
// OpStateError indicates an impossible internal state has been reached in an [Op].
type OpStateError string
func (o OpStateError) Error() string { return "impossible " + string(o) + " state reached" }
// initParams are params passed from parent. // initParams are params passed from parent.
type initParams struct { type initParams struct {
Params Params
@ -118,7 +106,7 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
if errors.Is(err, EBADF) { if errors.Is(err, EBADF) {
k.fatal("invalid setup descriptor") k.fatal("invalid setup descriptor")
} }
if errors.Is(err, ErrReceiveEnv) { if errors.Is(err, ErrNotSet) {
k.fatal("HAKUREI_SETUP not set") k.fatal("HAKUREI_SETUP not set")
} }
@ -186,11 +174,10 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
} }
if err := op.early(state, k); err != nil { if err := op.early(state, k); err != nil {
if m, ok := messageFromError(err); ok { k.printBaseErr(err,
k.fatal(m) fmt.Sprintf("cannot prepare op at index %d:", i))
} else { k.beforeExit()
k.fatalf("cannot prepare op at index %d: %v", i, err) k.exit(1)
}
} }
} }
@ -225,15 +212,12 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
chdir is allowed but discouraged */ chdir is allowed but discouraged */
for i, op := range *params.Ops { for i, op := range *params.Ops {
// ops already checked during early setup // ops already checked during early setup
if prefix, ok := op.prefix(); ok { k.verbosef("%s %s", op.prefix(), op)
k.verbosef("%s %s", prefix, op)
}
if err := op.apply(state, k); err != nil { if err := op.apply(state, k); err != nil {
if m, ok := messageFromError(err); ok { k.printBaseErr(err,
k.fatal(m) fmt.Sprintf("cannot apply op at index %d:", i))
} else { k.beforeExit()
k.fatalf("cannot apply op at index %d: %v", i, err) k.exit(1)
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,7 @@ func (b *BindMountOp) Valid() bool {
func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error { func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
if b.Flags&BindEnsure != 0 { if b.Flags&BindEnsure != 0 {
if err := k.mkdirAll(b.Source.String(), 0700); err != nil { if err := k.mkdirAll(b.Source.String(), 0700); err != nil {
return err return wrapErrSelf(err)
} }
} }
@ -52,7 +52,7 @@ func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
// leave sourceFinal as nil // leave sourceFinal as nil
return nil return nil
} }
return err return wrapErrSelf(err)
} else { } else {
b.sourceFinal, err = NewAbs(pathname) b.sourceFinal, err = NewAbs(pathname)
return err return err
@ -63,7 +63,7 @@ func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
if b.sourceFinal == nil { if b.sourceFinal == nil {
if b.Flags&BindOptional == 0 { if b.Flags&BindOptional == 0 {
// unreachable // unreachable
return OpStateError("bind") return msg.WrapErr(os.ErrClosed, "impossible bind state reached")
} }
return nil return nil
} }
@ -74,10 +74,10 @@ func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
// this perm value emulates bwrap behaviour as it clears bits from 0755 based on // this perm value emulates bwrap behaviour as it clears bits from 0755 based on
// op->perms which is never set for any bind setup op so always results in 0700 // op->perms which is never set for any bind setup op so always results in 0700
if fi, err := k.stat(source); err != nil { if fi, err := k.stat(source); err != nil {
return err return wrapErrSelf(err)
} else if fi.IsDir() { } else if fi.IsDir() {
if err = k.mkdirAll(target, 0700); err != nil { if err = k.mkdirAll(target, 0700); err != nil {
return err return wrapErrSelf(err)
} }
} else if err = k.ensureFile(target, 0444, 0700); err != nil { } else if err = k.ensureFile(target, 0444, 0700); err != nil {
return err return err
@ -91,12 +91,7 @@ func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
flags |= syscall.MS_NODEV flags |= syscall.MS_NODEV
} }
if b.sourceFinal.String() == b.Target.String() { return k.bindMount(source, target, flags, b.sourceFinal == b.Target)
k.verbosef("mounting %q flags %#x", target, flags)
} else {
k.verbosef("mounting %q on %q flags %#x", source, target, flags)
}
return k.bindMount(source, target, flags)
} }
func (b *BindMountOp) Is(op Op) bool { func (b *BindMountOp) Is(op Op) bool {
@ -106,7 +101,7 @@ func (b *BindMountOp) Is(op Op) bool {
b.Target.Is(vb.Target) && b.Target.Is(vb.Target) &&
b.Flags == vb.Flags b.Flags == vb.Flags
} }
func (*BindMountOp) prefix() (string, bool) { return "mounting", false } func (*BindMountOp) prefix() string { return "mounting" }
func (b *BindMountOp) String() string { func (b *BindMountOp) String() string {
if b.Source == nil || b.Target == nil { if b.Source == nil || b.Target == nil {
return "<invalid>" return "<invalid>"

View File

@ -5,8 +5,6 @@ import (
"os" "os"
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestBindMountOp(t *testing.T) { func TestBindMountOp(t *testing.T) {
@ -14,156 +12,138 @@ func TestBindMountOp(t *testing.T) {
{"ENOENT not optional", new(Params), &BindMountOp{ {"ENOENT not optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT), {"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT},
}, syscall.ENOENT, nil, nil}, }, wrapErrSelf(syscall.ENOENT), nil, nil},
{"skip optional", new(Params), &BindMountOp{ {"skip optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
Flags: BindOptional, Flags: BindOptional,
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT), {"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT},
}, nil, nil, nil}, }, nil, nil, nil},
{"success optional", new(Params), &BindMountOp{ {"success optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
Flags: BindOptional, Flags: BindOptional,
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil), {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
}, nil}, }, nil},
{"ensureFile device", new(Params), &BindMountOp{ {"ensureFile device", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"), Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"), Target: MustAbs("/dev/null"),
Flags: BindWritable | BindDevice, Flags: BindWritable | BindDevice,
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil), {"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil), {"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, stub.UniqueError(5)), {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique},
}, stub.UniqueError(5)}, }, errUnique},
{"mkdirAll ensure", new(Params), &BindMountOp{ {"mkdirAll ensure", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
Flags: BindEnsure, Flags: BindEnsure,
}, []stub.Call{ }, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, stub.UniqueError(4)), {"mkdirAll", expectArgs{"/bin/", os.FileMode(0700)}, nil, errUnique},
}, stub.UniqueError(4), nil, nil}, }, wrapErrSelf(errUnique), nil, nil},
{"success ensure", new(Params), &BindMountOp{ {"success ensure", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/usr/bin/"), Target: MustAbs("/usr/bin/"),
Flags: BindEnsure, Flags: BindEnsure,
}, []stub.Call{ }, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, nil), {"mkdirAll", expectArgs{"/bin/", os.FileMode(0700)}, nil, nil},
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
call("mkdirAll", stub.ExpectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil},
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005)}}, nil, nil), {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, nil, nil},
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, nil, nil),
}, nil}, }, nil},
{"success device ro", new(Params), &BindMountOp{ {"success device ro", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"), Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"), Target: MustAbs("/dev/null"),
Flags: BindDevice, Flags: BindDevice,
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil), {"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil), {"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil), {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/dev/null", uintptr(0x4001)}}, nil, nil), {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, nil, nil},
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, nil, nil),
}, nil}, }, nil},
{"success device", new(Params), &BindMountOp{ {"success device", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"), Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"), Target: MustAbs("/dev/null"),
Flags: BindWritable | BindDevice, Flags: BindWritable | BindDevice,
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil), {"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil), {"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil), {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/dev/null", uintptr(0x4000)}}, nil, nil), {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil},
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil),
}, nil}, }, nil},
{"evalSymlinks", new(Params), &BindMountOp{ {"evalSymlinks", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", stub.UniqueError(3)), {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", errUnique},
}, stub.UniqueError(3), nil, nil}, }, wrapErrSelf(errUnique), nil, nil},
{"stat", new(Params), &BindMountOp{ {"stat", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), stub.UniqueError(2)), {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), errUnique},
}, stub.UniqueError(2)}, }, wrapErrSelf(errUnique)},
{"mkdirAll", new(Params), &BindMountOp{ {"mkdirAll", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, stub.UniqueError(1)), {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, errUnique},
}, stub.UniqueError(1)}, }, wrapErrSelf(errUnique)},
{"bindMount", new(Params), &BindMountOp{ {"bindMount", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil), {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, errUnique},
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, stub.UniqueError(0)), }, errUnique},
}, stub.UniqueError(0)},
{"success eval equals", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/bin", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/bin"}, isDirFi(true), nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
call("bindMount", stub.ExpectArgs{"/host/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
}, nil},
{"success", new(Params), &BindMountOp{ {"success", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil), {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
}, nil}, }, nil},
}) })
t.Run("unreachable", func(t *testing.T) { t.Run("unreachable", func(t *testing.T) {
t.Run("nil sourceFinal not optional", func(t *testing.T) { t.Run("nil sourceFinal not optional", func(t *testing.T) {
wantErr := OpStateError("bind") wantErr := msg.WrapErr(os.ErrClosed, "impossible bind state reached")
if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) { if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr) t.Errorf("apply: error = %v, want %v", err, wantErr)
} }

View File

@ -49,6 +49,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
toHost(FHSDev+name), toHost(FHSDev+name),
targetPath, targetPath,
0, 0,
true,
); err != nil { ); err != nil {
return err return err
} }
@ -58,7 +59,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
FHSProc+"self/fd/"+string(rune(i+'0')), FHSProc+"self/fd/"+string(rune(i+'0')),
path.Join(target, name), path.Join(target, name),
); err != nil { ); err != nil {
return err return wrapErrSelf(err)
} }
} }
for _, pair := range [][2]string{ for _, pair := range [][2]string{
@ -67,21 +68,21 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
{"pts/ptmx", "ptmx"}, {"pts/ptmx", "ptmx"},
} { } {
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil { if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
return err return wrapErrSelf(err)
} }
} }
devShmPath := path.Join(target, "shm")
devPtsPath := path.Join(target, "pts") devPtsPath := path.Join(target, "pts")
for _, name := range []string{devShmPath, devPtsPath} { for _, name := range []string{path.Join(target, "shm"), devPtsPath} {
if err := k.mkdir(name, state.ParentPerm); err != nil { if err := k.mkdir(name, state.ParentPerm); err != nil {
return err return wrapErrSelf(err)
} }
} }
if err := k.mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC, if err := k.mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC,
"newinstance,ptmxmode=0666,mode=620"); err != nil { "newinstance,ptmxmode=0666,mode=620"); err != nil {
return err return wrapErrSuffix(err,
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
} }
if state.RetainSession { if state.RetainSession {
@ -91,11 +92,12 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
return err return err
} }
if name, err := k.readlink(hostProc.stdout()); err != nil { if name, err := k.readlink(hostProc.stdout()); err != nil {
return err return wrapErrSelf(err)
} else if err = k.bindMount( } else if err = k.bindMount(
toHost(name), toHost(name),
consolePath, consolePath,
0, 0,
false,
); err != nil { ); err != nil {
return err return err
} }
@ -105,21 +107,18 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
if d.Mqueue { if d.Mqueue {
mqueueTarget := path.Join(target, "mqueue") mqueueTarget := path.Join(target, "mqueue")
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil { if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
return err return wrapErrSelf(err)
} }
if err := k.mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil { if err := k.mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil {
return err return wrapErrSuffix(err, "cannot mount mqueue:")
} }
} }
if d.Write { if d.Write {
return nil return nil
} }
return wrapErrSuffix(k.remount(target, MS_RDONLY),
if err := k.remount(target, MS_RDONLY); err != nil { fmt.Sprintf("cannot remount %q:", target))
return err
}
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
} }
func (d *MountDevOp) Is(op Op) bool { func (d *MountDevOp) Is(op Op) bool {
@ -129,7 +128,7 @@ func (d *MountDevOp) Is(op Op) bool {
d.Mqueue == vd.Mqueue && d.Mqueue == vd.Mqueue &&
d.Write == vd.Write d.Write == vd.Write
} }
func (*MountDevOp) prefix() (string, bool) { return "mounting", true } func (*MountDevOp) prefix() string { return "mounting" }
func (d *MountDevOp) String() string { func (d *MountDevOp) String() string {
if d.Mqueue { if d.Mqueue {
return fmt.Sprintf("dev on %q with mqueue", d.Target) return fmt.Sprintf("dev on %q with mqueue", d.Target)

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@ type MkdirOp struct {
func (m *MkdirOp) Valid() bool { return m != nil && m.Path != nil } func (m *MkdirOp) Valid() bool { return m != nil && m.Path != nil }
func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil } func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil }
func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error { func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error {
return k.mkdirAll(toSysroot(m.Path.String()), m.Perm) return wrapErrSelf(k.mkdirAll(toSysroot(m.Path.String()), m.Perm))
} }
func (m *MkdirOp) Is(op Op) bool { func (m *MkdirOp) Is(op Op) bool {
@ -32,5 +32,5 @@ func (m *MkdirOp) Is(op Op) bool {
m.Path.Is(vm.Path) && m.Path.Is(vm.Path) &&
m.Perm == vm.Perm m.Perm == vm.Perm
} }
func (*MkdirOp) prefix() (string, bool) { return "creating", true } func (*MkdirOp) prefix() string { return "creating" }
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) } func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }

View File

@ -3,8 +3,6 @@ package container
import ( import (
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestMkdirOp(t *testing.T) { func TestMkdirOp(t *testing.T) {
@ -12,8 +10,8 @@ func TestMkdirOp(t *testing.T) {
{"success", new(Params), &MkdirOp{ {"success", new(Params), &MkdirOp{
Path: MustAbs("/.hakurei"), Path: MustAbs("/.hakurei"),
Perm: 0500, Perm: 0500,
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/.hakurei", os.FileMode(0500)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/.hakurei", os.FileMode(0500)}, nil, nil},
}, nil}, }, nil},
}) })

View File

@ -3,6 +3,7 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"io/fs"
"slices" "slices"
"strings" "strings"
) )
@ -18,39 +19,6 @@ const (
func init() { gob.Register(new(MountOverlayOp)) } func init() { gob.Register(new(MountOverlayOp)) }
const (
// OverlayEphemeralUnexpectedUpper is set when [MountOverlayOp.Work] is nil
// and [MountOverlayOp.Upper] holds an unexpected value.
OverlayEphemeralUnexpectedUpper = iota
// OverlayReadonlyLower is set when [MountOverlayOp.Lower] contains less than
// two entries when mounting readonly.
OverlayReadonlyLower
// OverlayEmptyLower is set when [MountOverlayOp.Lower] has length of zero.
OverlayEmptyLower
)
// OverlayArgumentError is returned for [MountOverlayOp] supplied with invalid argument.
type OverlayArgumentError struct {
Type uintptr
Value string
}
func (e *OverlayArgumentError) Error() string {
switch e.Type {
case OverlayEphemeralUnexpectedUpper:
return fmt.Sprintf("upperdir has unexpected value %q", e.Value)
case OverlayReadonlyLower:
return "readonly overlay requires at least two lowerdir"
case OverlayEmptyLower:
return "overlay requires at least one lowerdir"
default:
return fmt.Sprintf("invalid overlay argument error %#x", e.Type)
}
}
// Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]. // Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target].
func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops { func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops {
*f = append(*f, &MountOverlayOp{ *f = append(*f, &MountOverlayOp{
@ -121,7 +89,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
o.ephemeral = true // intermediate root not yet available o.ephemeral = true // intermediate root not yet available
default: default:
return &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, o.Upper.String()} return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("upperdir has unexpected value %q", o.Upper))
} }
} }
// readonly handled in apply // readonly handled in apply
@ -129,12 +97,12 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if !o.ephemeral { if !o.ephemeral {
if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) { if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) {
// unreachable // unreachable
return OpStateError("overlay") return msg.WrapErr(fs.ErrClosed, "impossible overlay state reached")
} }
if o.Upper != nil { if o.Upper != nil {
if v, err := k.evalSymlinks(o.Upper.String()); err != nil { if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
return err return wrapErrSelf(err)
} else { } else {
o.upper = EscapeOverlayDataSegment(toHost(v)) o.upper = EscapeOverlayDataSegment(toHost(v))
} }
@ -142,7 +110,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if o.Work != nil { if o.Work != nil {
if v, err := k.evalSymlinks(o.Work.String()); err != nil { if v, err := k.evalSymlinks(o.Work.String()); err != nil {
return err return wrapErrSelf(err)
} else { } else {
o.work = EscapeOverlayDataSegment(toHost(v)) o.work = EscapeOverlayDataSegment(toHost(v))
} }
@ -152,7 +120,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
o.lower = make([]string, len(o.Lower)) o.lower = make([]string, len(o.Lower))
for i, a := range o.Lower { // nil checked in Valid for i, a := range o.Lower { // nil checked in Valid
if v, err := k.evalSymlinks(a.String()); err != nil { if v, err := k.evalSymlinks(a.String()); err != nil {
return err return wrapErrSelf(err)
} else { } else {
o.lower[i] = EscapeOverlayDataSegment(toHost(v)) o.lower[i] = EscapeOverlayDataSegment(toHost(v))
} }
@ -166,17 +134,17 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
target = toSysroot(target) target = toSysroot(target)
} }
if err := k.mkdirAll(target, state.ParentPerm); err != nil { if err := k.mkdirAll(target, state.ParentPerm); err != nil {
return err return wrapErrSelf(err)
} }
if o.ephemeral { if o.ephemeral {
var err error var err error
// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed // these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
if o.upper, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil { if o.upper, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil {
return err return wrapErrSelf(err)
} }
if o.work, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil { if o.work, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil {
return err return wrapErrSelf(err)
} }
} }
@ -184,12 +152,12 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
if o.upper == zeroString && o.work == zeroString { // readonly if o.upper == zeroString && o.work == zeroString { // readonly
if len(o.Lower) < 2 { if len(o.Lower) < 2 {
return &OverlayArgumentError{OverlayReadonlyLower, zeroString} return msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir")
} }
// "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only // "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only
} else { } else {
if len(o.Lower) == 0 { if len(o.Lower) == 0 {
return &OverlayArgumentError{OverlayEmptyLower, zeroString} return msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir")
} }
options = append(options, options = append(options,
OptionOverlayUpperdir+"="+o.upper, OptionOverlayUpperdir+"="+o.upper,
@ -199,7 +167,8 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath), OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath),
OptionOverlayUserxattr) OptionOverlayUserxattr)
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)) return wrapErrSuffix(k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)),
fmt.Sprintf("cannot mount overlay on %q:", o.Target))
} }
func (o *MountOverlayOp) Is(op Op) bool { func (o *MountOverlayOp) Is(op Op) bool {
@ -209,7 +178,7 @@ func (o *MountOverlayOp) Is(op Op) bool {
slices.EqualFunc(o.Lower, vo.Lower, func(a *Absolute, v *Absolute) bool { return a.Is(v) }) && slices.EqualFunc(o.Lower, vo.Lower, func(a *Absolute, v *Absolute) bool { return a.Is(v) }) &&
o.Upper.Is(vo.Upper) && o.Work.Is(vo.Work) o.Upper.Is(vo.Upper) && o.Work.Is(vo.Work)
} }
func (*MountOverlayOp) prefix() (string, bool) { return "mounting", true } func (*MountOverlayOp) prefix() string { return "mounting" }
func (o *MountOverlayOp) String() string { func (o *MountOverlayOp) String() string {
return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower)) return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower))
} }

View File

@ -2,40 +2,12 @@ package container
import ( import (
"errors" "errors"
"io/fs"
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestMountOverlayOp(t *testing.T) { func TestMountOverlayOp(t *testing.T) {
t.Run("argument error", func(t *testing.T) {
testCases := []struct {
name string
err *OverlayArgumentError
want string
}{
{"unexpected upper", &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, "/proc/"},
`upperdir has unexpected value "/proc/"`},
{"lower ro short", &OverlayArgumentError{OverlayReadonlyLower, zeroString},
"readonly overlay requires at least two lowerdir"},
{"lower short", &OverlayArgumentError{OverlayEmptyLower, zeroString},
"overlay requires at least one lowerdir"},
{"oob", &OverlayArgumentError{0xdeadbeef, zeroString},
"invalid overlay argument error 0xdeadbeef"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want)
}
})
}
})
checkOpBehaviour(t, []opBehaviourTestCase{ checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdirTemp invalid ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{ {"mkdirTemp invalid ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"), Target: MustAbs("/"),
@ -44,7 +16,7 @@ func TestMountOverlayOp(t *testing.T) {
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"), MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
}, },
Upper: MustAbs("/proc/"), Upper: MustAbs("/proc/"),
}, nil, &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, "/proc/"}, nil, nil}, }, nil, msg.WrapErr(fs.ErrInvalid, `upperdir has unexpected value "/proc/"`), nil, nil},
{"mkdirTemp upper ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{ {"mkdirTemp upper ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"), Target: MustAbs("/"),
@ -53,13 +25,13 @@ func TestMountOverlayOp(t *testing.T) {
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"), MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
}, },
Upper: MustAbs("/"), Upper: MustAbs("/"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", stub.UniqueError(6)), {"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", errUnique},
}, stub.UniqueError(6)}, }, wrapErrSelf(errUnique)},
{"mkdirTemp work ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{ {"mkdirTemp work ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"), Target: MustAbs("/"),
@ -68,14 +40,14 @@ func TestMountOverlayOp(t *testing.T) {
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"), MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
}, },
Upper: MustAbs("/"), Upper: MustAbs("/"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil), {"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil},
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", stub.UniqueError(5)), {"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", errUnique},
}, stub.UniqueError(5)}, }, wrapErrSelf(errUnique)},
{"success ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{ {"success ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"), Target: MustAbs("/"),
@ -84,20 +56,20 @@ func TestMountOverlayOp(t *testing.T) {
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"), MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
}, },
Upper: MustAbs("/"), Upper: MustAbs("/"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil), {"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil), {"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil},
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil), {"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil},
call("mount", stub.ExpectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" + {"mount", expectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" +
"upperdir=overlay.upper.32768," + "upperdir=overlay.upper.32768," +
"workdir=overlay.work.32768," + "workdir=overlay.work.32768," +
"lowerdir=" + "lowerdir=" +
`/host/var/lib/planterette/base/debian\:f92c9052:` + `/host/var/lib/planterette/base/debian\:f92c9052:` +
`/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` + `/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` +
"userxattr"}, nil, nil), "userxattr"}, nil, nil},
}, nil}, }, nil},
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{ {"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
@ -105,11 +77,11 @@ func TestMountOverlayOp(t *testing.T) {
Lower: []*Absolute{ Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"), MustAbs("/mnt-root/nix/.ro-store"),
}, },
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil},
}, &OverlayArgumentError{OverlayReadonlyLower, zeroString}}, }, msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir")},
{"success ro noPrefix", &Params{ParentPerm: 0755}, &MountOverlayOp{ {"success ro noPrefix", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
@ -118,16 +90,16 @@ func TestMountOverlayOp(t *testing.T) {
MustAbs("/mnt-root/nix/.ro-store0"), MustAbs("/mnt-root/nix/.ro-store0"),
}, },
noPrefix: true, noPrefix: true,
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/nix/store", os.FileMode(0755)}, nil, nil), {"mkdirAll", expectArgs{"/nix/store", os.FileMode(0755)}, nil, nil},
call("mount", stub.ExpectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" + {"mount", expectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" +
"lowerdir=" + "lowerdir=" +
"/host/mnt-root/nix/.ro-store:" + "/host/mnt-root/nix/.ro-store:" +
"/host/mnt-root/nix/.ro-store0," + "/host/mnt-root/nix/.ro-store0," +
"userxattr"}, nil, nil), "userxattr"}, nil, nil},
}, nil}, }, nil},
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{ {"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
@ -136,102 +108,102 @@ func TestMountOverlayOp(t *testing.T) {
MustAbs("/mnt-root/nix/.ro-store"), MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"), MustAbs("/mnt-root/nix/.ro-store0"),
}, },
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil},
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" + {"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"lowerdir=" + "lowerdir=" +
"/host/mnt-root/nix/.ro-store:" + "/host/mnt-root/nix/.ro-store:" +
"/host/mnt-root/nix/.ro-store0," + "/host/mnt-root/nix/.ro-store0," +
"userxattr"}, nil, nil), "userxattr"}, nil, nil},
}, nil}, }, nil},
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
}, &OverlayArgumentError{OverlayEmptyLower, zeroString}}, }, msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir")},
{"evalSymlinks upper", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"evalSymlinks upper", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", stub.UniqueError(4)), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", errUnique},
}, stub.UniqueError(4), nil, nil}, }, wrapErrSelf(errUnique), nil, nil},
{"evalSymlinks work", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"evalSymlinks work", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", stub.UniqueError(3)), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", errUnique},
}, stub.UniqueError(3), nil, nil}, }, wrapErrSelf(errUnique), nil, nil},
{"evalSymlinks lower", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"evalSymlinks lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", stub.UniqueError(2)), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", errUnique},
}, stub.UniqueError(2), nil, nil}, }, wrapErrSelf(errUnique), nil, nil},
{"mkdirAll", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"mkdirAll", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, stub.UniqueError(1)), {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, errUnique},
}, stub.UniqueError(1)}, }, wrapErrSelf(errUnique)},
{"mount", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"mount", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, stub.UniqueError(0)), {"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, errUnique},
}, stub.UniqueError(0)}, }, wrapErrSuffix(errUnique, `cannot mount overlay on "/nix/store":`)},
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" + {"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"upperdir=/host/mnt-root/nix/.rw-store/.upper," + "upperdir=/host/mnt-root/nix/.rw-store/.upper," +
"workdir=/host/mnt-root/nix/.rw-store/.work," + "workdir=/host/mnt-root/nix/.rw-store/.work," +
"lowerdir=/host/mnt-root/nix/ro-store," + "lowerdir=/host/mnt-root/nix/ro-store," +
"userxattr"}, nil, nil), "userxattr"}, nil, nil},
}, nil}, }, nil},
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
@ -245,17 +217,17 @@ func TestMountOverlayOp(t *testing.T) {
}, },
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{ }, []kexpect{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/ro-store0", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/ro-store0", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store1"}, "/mnt-root/nix/ro-store1", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store1"}, "/mnt-root/nix/ro-store1", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store2"}, "/mnt-root/nix/ro-store2", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store2"}, "/mnt-root/nix/ro-store2", nil},
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil), {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" + {"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"upperdir=/host/mnt-root/nix/.rw-store/.upper," + "upperdir=/host/mnt-root/nix/.rw-store/.upper," +
"workdir=/host/mnt-root/nix/.rw-store/.work," + "workdir=/host/mnt-root/nix/.rw-store/.work," +
"lowerdir=" + "lowerdir=" +
@ -264,13 +236,13 @@ func TestMountOverlayOp(t *testing.T) {
"/host/mnt-root/nix/ro-store1:" + "/host/mnt-root/nix/ro-store1:" +
"/host/mnt-root/nix/ro-store2:" + "/host/mnt-root/nix/ro-store2:" +
"/host/mnt-root/nix/ro-store3," + "/host/mnt-root/nix/ro-store3," +
"userxattr"}, nil, nil), "userxattr"}, nil, nil},
}, nil}, }, nil},
}) })
t.Run("unreachable", func(t *testing.T) { t.Run("unreachable", func(t *testing.T) {
t.Run("nil Upper non-nil Work not ephemeral", func(t *testing.T) { t.Run("nil Upper non-nil Work not ephemeral", func(t *testing.T) {
wantErr := OpStateError("overlay") wantErr := msg.WrapErr(fs.ErrClosed, "impossible overlay state reached")
if err := (&MountOverlayOp{ if err := (&MountOverlayOp{
Work: MustAbs("/"), Work: MustAbs("/"),
}).early(nil, nil); !errors.Is(err, wantErr) { }).early(nil, nil); !errors.Is(err, wantErr) {

View File

@ -39,11 +39,13 @@ func (t *TmpfileOp) early(*setupState, syscallDispatcher) error { return nil }
func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error { func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
var tmpPath string var tmpPath string
if f, err := k.createTemp(FHSRoot, intermediatePatternTmpfile); err != nil { if f, err := k.createTemp(FHSRoot, intermediatePatternTmpfile); err != nil {
return err return wrapErrSelf(err)
} else if _, err = f.Write(t.Data); err != nil { } else if _, err = f.Write(t.Data); err != nil {
return err return wrapErrSuffix(err,
"cannot write to intermediate file:")
} else if err = f.Close(); err != nil { } else if err = f.Close(); err != nil {
return err return wrapErrSuffix(err,
"cannot close intermediate file:")
} else { } else {
tmpPath = f.Name() tmpPath = f.Name()
} }
@ -55,10 +57,11 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
tmpPath, tmpPath,
target, target,
syscall.MS_RDONLY|syscall.MS_NODEV, syscall.MS_RDONLY|syscall.MS_NODEV,
false,
); err != nil { ); err != nil {
return err return err
} else if err = k.remove(tmpPath); err != nil { } else if err = k.remove(tmpPath); err != nil {
return err return wrapErrSelf(err)
} }
return nil return nil
} }
@ -69,7 +72,7 @@ func (t *TmpfileOp) Is(op Op) bool {
t.Path.Is(vt.Path) && t.Path.Is(vt.Path) &&
string(t.Data) == string(vt.Data) string(t.Data) == string(vt.Data)
} }
func (*TmpfileOp) prefix() (string, bool) { return "placing", true } func (*TmpfileOp) prefix() string { return "placing" }
func (t *TmpfileOp) String() string { func (t *TmpfileOp) String() string {
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data)) return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
} }

View File

@ -3,8 +3,6 @@ package container
import ( import (
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestTmpfileOp(t *testing.T) { func TestTmpfileOp(t *testing.T) {
@ -18,59 +16,59 @@ func TestTmpfileOp(t *testing.T) {
{"createTemp", &Params{ParentPerm: 0700}, &TmpfileOp{ {"createTemp", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), stub.UniqueError(5)), {"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), errUnique},
}, stub.UniqueError(5)}, }, wrapErrSelf(errUnique)},
{"Write", &Params{ParentPerm: 0700}, &TmpfileOp{ {"Write", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, writeErrOsFile{stub.UniqueError(4)}, nil), {"createTemp", expectArgs{"/", "tmp.*"}, writeErrOsFile{errUnique}, nil},
}, stub.UniqueError(4)}, }, wrapErrSuffix(errUnique, "cannot write to intermediate file:")},
{"Close", &Params{ParentPerm: 0700}, &TmpfileOp{ {"Close", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, stub.UniqueError(3)), nil), {"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, errUnique), nil},
}, stub.UniqueError(3)}, }, wrapErrSuffix(errUnique, "cannot close intermediate file:")},
{"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{ {"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil), {"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, stub.UniqueError(2)), {"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique},
}, stub.UniqueError(2)}, }, errUnique},
{"bindMount", &Params{ParentPerm: 0700}, &TmpfileOp{ {"bindMount", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil), {"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil), {"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, stub.UniqueError(1)), {"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, errUnique},
}, stub.UniqueError(1)}, }, errUnique},
{"remove", &Params{ParentPerm: 0700}, &TmpfileOp{ {"remove", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil), {"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil), {"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil), {"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil},
call("remove", stub.ExpectArgs{"tmp.32768"}, nil, stub.UniqueError(0)), {"remove", expectArgs{"tmp.32768"}, nil, errUnique},
}, stub.UniqueError(0)}, }, wrapErrSelf(errUnique)},
{"success", &Params{ParentPerm: 0700}, &TmpfileOp{ {"success", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil), {"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil), {"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil), {"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil},
call("remove", stub.ExpectArgs{"tmp.32768"}, nil, nil), {"remove", expectArgs{"tmp.32768"}, nil, nil},
}, nil}, }, nil},
}) })

View File

@ -22,9 +22,10 @@ func (p *MountProcOp) early(*setupState, syscallDispatcher) error { return nil }
func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error { func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error {
target := toSysroot(p.Target.String()) target := toSysroot(p.Target.String())
if err := k.mkdirAll(target, state.ParentPerm); err != nil { if err := k.mkdirAll(target, state.ParentPerm); err != nil {
return err return wrapErrSelf(err)
} }
return k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString) return wrapErrSuffix(k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString),
fmt.Sprintf("cannot mount proc on %q:", p.Target.String()))
} }
func (p *MountProcOp) Is(op Op) bool { func (p *MountProcOp) Is(op Op) bool {
@ -32,5 +33,5 @@ func (p *MountProcOp) Is(op Op) bool {
return ok && p.Valid() && vp.Valid() && return ok && p.Valid() && vp.Valid() &&
p.Target.Is(vp.Target) p.Target.Is(vp.Target)
} }
func (*MountProcOp) prefix() (string, bool) { return "mounting", true } func (*MountProcOp) prefix() string { return "mounting" }
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) } func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }

View File

@ -3,8 +3,6 @@ package container
import ( import (
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestMountProcOp(t *testing.T) { func TestMountProcOp(t *testing.T) {
@ -12,16 +10,16 @@ func TestMountProcOp(t *testing.T) {
{"mkdir", &Params{ParentPerm: 0755}, {"mkdir", &Params{ParentPerm: 0755},
&MountProcOp{ &MountProcOp{
Target: MustAbs("/proc/"), Target: MustAbs("/proc/"),
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, stub.UniqueError(0)), {"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, errUnique},
}, stub.UniqueError(0)}, }, wrapErrSelf(errUnique)},
{"success", &Params{ParentPerm: 0700}, {"success", &Params{ParentPerm: 0700},
&MountProcOp{ &MountProcOp{
Target: MustAbs("/proc/"), Target: MustAbs("/proc/"),
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil},
call("mount", stub.ExpectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil), {"mount", expectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil},
}, nil}, }, nil},
}) })

View File

@ -22,7 +22,8 @@ type RemountOp struct {
func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil } func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil }
func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil } func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
func (r *RemountOp) apply(_ *setupState, k syscallDispatcher) error { func (r *RemountOp) apply(_ *setupState, k syscallDispatcher) error {
return k.remount(toSysroot(r.Target.String()), r.Flags) return wrapErrSuffix(k.remount(toSysroot(r.Target.String()), r.Flags),
fmt.Sprintf("cannot remount %q:", r.Target))
} }
func (r *RemountOp) Is(op Op) bool { func (r *RemountOp) Is(op Op) bool {
@ -31,5 +32,5 @@ func (r *RemountOp) Is(op Op) bool {
r.Target.Is(vr.Target) && r.Target.Is(vr.Target) &&
r.Flags == vr.Flags r.Flags == vr.Flags
} }
func (*RemountOp) prefix() (string, bool) { return "remounting", true } func (*RemountOp) prefix() string { return "remounting" }
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) } func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }

View File

@ -3,8 +3,6 @@ package container
import ( import (
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestRemountOp(t *testing.T) { func TestRemountOp(t *testing.T) {
@ -12,8 +10,8 @@ func TestRemountOp(t *testing.T) {
{"success", new(Params), &RemountOp{ {"success", new(Params), &RemountOp{
Target: MustAbs("/"), Target: MustAbs("/"),
Flags: syscall.MS_RDONLY, Flags: syscall.MS_RDONLY,
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("remount", stub.ExpectArgs{"/sysroot", uintptr(1)}, nil, nil), {"remount", expectArgs{"/sysroot", uintptr(1)}, nil, nil},
}, nil}, }, nil},
}) })

View File

@ -3,6 +3,7 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"io/fs"
"path" "path"
) )
@ -29,10 +30,10 @@ func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkN
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error { func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
if l.Dereference { if l.Dereference {
if !isAbs(l.LinkName) { if !isAbs(l.LinkName) {
return &AbsoluteError{l.LinkName} return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("path %q is not absolute", l.LinkName))
} }
if name, err := k.readlink(l.LinkName); err != nil { if name, err := k.readlink(l.LinkName); err != nil {
return err return wrapErrSelf(err)
} else { } else {
l.LinkName = name l.LinkName = name
} }
@ -43,9 +44,9 @@ func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error { func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
target := toSysroot(l.Target.String()) target := toSysroot(l.Target.String())
if err := k.mkdirAll(path.Dir(target), state.ParentPerm); err != nil { if err := k.mkdirAll(path.Dir(target), state.ParentPerm); err != nil {
return err return wrapErrSelf(err)
} }
return k.symlink(l.LinkName, target) return wrapErrSelf(k.symlink(l.LinkName, target))
} }
func (l *SymlinkOp) Is(op Op) bool { func (l *SymlinkOp) Is(op Op) bool {
@ -55,7 +56,7 @@ func (l *SymlinkOp) Is(op Op) bool {
l.LinkName == vl.LinkName && l.LinkName == vl.LinkName &&
l.Dereference == vl.Dereference l.Dereference == vl.Dereference
} }
func (*SymlinkOp) prefix() (string, bool) { return "creating", true } func (*SymlinkOp) prefix() string { return "creating" }
func (l *SymlinkOp) String() string { func (l *SymlinkOp) String() string {
return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName) return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName)
} }

View File

@ -1,10 +1,9 @@
package container package container
import ( import (
"io/fs"
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestSymlinkOp(t *testing.T) { func TestSymlinkOp(t *testing.T) {
@ -12,41 +11,41 @@ func TestSymlinkOp(t *testing.T) {
{"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{ {"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: MustAbs("/etc/nixos"), Target: MustAbs("/etc/nixos"),
LinkName: "/etc/static/nixos", LinkName: "/etc/static/nixos",
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, stub.UniqueError(1)), {"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, errUnique},
}, stub.UniqueError(1)}, }, wrapErrSelf(errUnique)},
{"abs", &Params{ParentPerm: 0755}, &SymlinkOp{ {"abs", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: MustAbs("/etc/mtab"), Target: MustAbs("/etc/mtab"),
LinkName: "etc/mtab", LinkName: "etc/mtab",
Dereference: true, Dereference: true,
}, nil, &AbsoluteError{"etc/mtab"}, nil, nil}, }, nil, msg.WrapErr(fs.ErrInvalid, `path "etc/mtab" is not absolute`), nil, nil},
{"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{ {"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: MustAbs("/etc/mtab"), Target: MustAbs("/etc/mtab"),
LinkName: "/etc/mtab", LinkName: "/etc/mtab",
Dereference: true, Dereference: true,
}, []stub.Call{ }, []kexpect{
call("readlink", stub.ExpectArgs{"/etc/mtab"}, "/proc/mounts", stub.UniqueError(0)), {"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", errUnique},
}, stub.UniqueError(0), nil, nil}, }, wrapErrSelf(errUnique), nil, nil},
{"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{ {"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: MustAbs("/etc/nixos"), Target: MustAbs("/etc/nixos"),
LinkName: "/etc/static/nixos", LinkName: "/etc/static/nixos",
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil},
call("symlink", stub.ExpectArgs{"/etc/static/nixos", "/sysroot/etc/nixos"}, nil, nil), {"symlink", expectArgs{"/etc/static/nixos", "/sysroot/etc/nixos"}, nil, nil},
}, nil}, }, nil},
{"success", &Params{ParentPerm: 0755}, &SymlinkOp{ {"success", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: MustAbs("/etc/mtab"), Target: MustAbs("/etc/mtab"),
LinkName: "/etc/mtab", LinkName: "/etc/mtab",
Dereference: true, Dereference: true,
}, []stub.Call{ }, []kexpect{
call("readlink", stub.ExpectArgs{"/etc/mtab"}, "/proc/mounts", nil), {"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", nil},
}, nil, []stub.Call{ }, nil, []kexpect{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0755)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0755)}, nil, nil},
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil), {"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
}, nil}, }, nil},
}) })

View File

@ -3,20 +3,14 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"io/fs"
"math" "math"
"os" "os"
"strconv"
. "syscall" . "syscall"
) )
func init() { gob.Register(new(MountTmpfsOp)) } func init() { gob.Register(new(MountTmpfsOp)) }
type TmpfsSizeError int
func (e TmpfsSizeError) Error() string {
return "tmpfs size " + strconv.Itoa(int(e)) + " out of bounds"
}
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path]. // Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops { func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm}) *f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm})
@ -42,7 +36,7 @@ func (t *MountTmpfsOp) Valid() bool { return t !=
func (t *MountTmpfsOp) early(*setupState, syscallDispatcher) error { return nil } func (t *MountTmpfsOp) early(*setupState, syscallDispatcher) error { return nil }
func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error { func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error {
if t.Size < 0 || t.Size > math.MaxUint>>1 { if t.Size < 0 || t.Size > math.MaxUint>>1 {
return TmpfsSizeError(t.Size) return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("size %d out of bounds", t.Size))
} }
return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm) return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
} }
@ -56,5 +50,5 @@ func (t *MountTmpfsOp) Is(op Op) bool {
t.Size == vt.Size && t.Size == vt.Size &&
t.Perm == vt.Perm t.Perm == vt.Perm
} }
func (*MountTmpfsOp) prefix() (string, bool) { return "mounting", true } func (*MountTmpfsOp) prefix() string { return "mounting" }
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) } func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }

View File

@ -1,40 +1,31 @@
package container package container
import ( import (
"io/fs"
"os" "os"
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestMountTmpfsOp(t *testing.T) { func TestMountTmpfsOp(t *testing.T) {
t.Run("size error", func(t *testing.T) {
tmpfsSizeError := TmpfsSizeError(-1)
want := "tmpfs size -1 out of bounds"
if got := tmpfsSizeError.Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
checkOpBehaviour(t, []opBehaviourTestCase{ checkOpBehaviour(t, []opBehaviourTestCase{
{"size oob", new(Params), &MountTmpfsOp{ {"size oob", new(Params), &MountTmpfsOp{
Size: -1, Size: -1,
}, nil, nil, nil, TmpfsSizeError(-1)}, }, nil, nil, nil, msg.WrapErr(fs.ErrInvalid, "size -1 out of bounds")},
{"success", new(Params), &MountTmpfsOp{ {"success", new(Params), &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: MustAbs("/run/user/1000/"), Path: MustAbs("/run/user/1000/"),
Size: 1 << 10, Size: 1 << 10,
Perm: 0700, Perm: 0700,
}, nil, nil, []stub.Call{ }, nil, nil, []kexpect{
call("mountTmpfs", stub.ExpectArgs{ {"mountTmpfs", expectArgs{
"ephemeral", // fsname "ephemeral", // fsname
"/sysroot/run/user/1000", // target "/sysroot/run/user/1000", // target
uintptr(0), // flags uintptr(0), // flags
0x400, // size 0x400, // size
os.FileMode(0700), // perm os.FileMode(0700), // perm
}, nil, nil), }, nil, nil},
}, nil}, }, nil},
}) })

View File

@ -43,8 +43,6 @@ const (
// Note that any source value is allowed when fstype is [FstypeOverlay]. // Note that any source value is allowed when fstype is [FstypeOverlay].
SourceOverlay = "overlay" SourceOverlay = "overlay"
// SourceTmpfs is used when mounting tmpfs.
SourceTmpfs = "tmpfs"
// SourceTmpfsRootfs is used when mounting the tmpfs instance backing the intermediate root. // SourceTmpfsRootfs is used when mounting the tmpfs instance backing the intermediate root.
SourceTmpfsRootfs = "rootfs" SourceTmpfsRootfs = "rootfs"
// SourceTmpfsDevtmpfs is used when mounting tmpfs representing a subset of host devtmpfs. // SourceTmpfsDevtmpfs is used when mounting tmpfs representing a subset of host devtmpfs.
@ -96,11 +94,18 @@ const (
) )
// bindMount mounts source on target and recursively applies flags if MS_REC is set. // bindMount mounts source on target and recursively applies flags if MS_REC is set.
func (p *procPaths) bindMount(source, target string, flags uintptr) error { func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
// syscallDispatcher.bindMount and procPaths.remount must not be called from this function // syscallDispatcher.bindMount and procPaths.remount must not be called from this function
if eq {
p.k.verbosef("resolved %q flags %#x", target, flags)
} else {
p.k.verbosef("resolved %q on %q flags %#x", source, target, flags)
}
if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil { if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
return err return wrapErrSuffix(err,
fmt.Sprintf("cannot mount %q on %q:", source, target))
} }
return p.k.remount(target, flags) return p.k.remount(target, flags)
@ -112,7 +117,7 @@ func (p *procPaths) remount(target string, flags uintptr) error {
var targetFinal string var targetFinal string
if v, err := p.k.evalSymlinks(target); err != nil { if v, err := p.k.evalSymlinks(target); err != nil {
return err return wrapErrSelf(err)
} else { } else {
targetFinal = v targetFinal = v
if targetFinal != target { if targetFinal != target {
@ -128,12 +133,14 @@ func (p *procPaths) remount(target string, flags uintptr) error {
destFd, err = p.k.open(targetFinal, O_PATH|O_CLOEXEC, 0) destFd, err = p.k.open(targetFinal, O_PATH|O_CLOEXEC, 0)
return return
}); err != nil { }); err != nil {
return &os.PathError{Op: "open", Path: targetFinal, Err: err} return wrapErrSuffix(err,
fmt.Sprintf("cannot open %q:", targetFinal))
} }
if v, err := p.k.readlink(p.fd(destFd)); err != nil { if v, err := p.k.readlink(p.fd(destFd)); err != nil {
return err return wrapErrSelf(err)
} else if err = p.k.close(destFd); err != nil { } else if err = p.k.close(destFd); err != nil {
return &os.PathError{Op: "close", Path: targetFinal, Err: err} return wrapErrSuffix(err,
fmt.Sprintf("cannot close %q:", targetFinal))
} else { } else {
targetKFinal = v targetKFinal = v
} }
@ -143,11 +150,17 @@ func (p *procPaths) remount(target string, flags uintptr) error {
return p.mountinfo(func(d *vfs.MountInfoDecoder) error { return p.mountinfo(func(d *vfs.MountInfoDecoder) error {
n, err := d.Unfold(targetKFinal) n, err := d.Unfold(targetKFinal)
if err != nil { if err != nil {
return err if errors.Is(err, ESTALE) {
return msg.WrapErr(err,
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
}
return wrapErrSuffix(err,
"cannot unfold mount hierarchy:")
} }
if err = remountWithFlags(p.k, n, mf); err != nil { if err = remountWithFlags(p.k, n, mf); err != nil {
return err return wrapErrSuffix(err,
fmt.Sprintf("cannot remount %q:", n.Clean))
} }
if flags&MS_REC == 0 { if flags&MS_REC == 0 {
return nil return nil
@ -159,8 +172,11 @@ func (p *procPaths) remount(target string, flags uintptr) error {
continue continue
} }
if err = remountWithFlags(p.k, cur, mf); err != nil && !errors.Is(err, EACCES) { err = remountWithFlags(p.k, cur, mf)
return err
if err != nil && !errors.Is(err, EACCES) {
return wrapErrSuffix(err,
fmt.Sprintf("cannot propagate flags to %q:", cur.Clean))
} }
} }
@ -189,13 +205,15 @@ func mountTmpfs(k syscallDispatcher, fsname, target string, flags uintptr, size
// syscallDispatcher.mountTmpfs must not be called from this function // syscallDispatcher.mountTmpfs must not be called from this function
if err := k.mkdirAll(target, parentPerm(perm)); err != nil { if err := k.mkdirAll(target, parentPerm(perm)); err != nil {
return err return wrapErrSelf(err)
} }
opt := fmt.Sprintf("mode=%#o", perm) opt := fmt.Sprintf("mode=%#o", perm)
if size > 0 { if size > 0 {
opt += fmt.Sprintf(",size=%d", size) opt += fmt.Sprintf(",size=%d", size)
} }
return k.mount(fsname, target, FstypeTmpfs, flags, opt) return wrapErrSuffix(
k.mount(fsname, target, FstypeTmpfs, flags, opt),
fmt.Sprintf("cannot mount tmpfs on %q:", target))
} }
func parentPerm(perm os.FileMode) os.FileMode { func parentPerm(perm os.FileMode) os.FileMode {

View File

@ -5,30 +5,32 @@ import (
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container/stub"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
) )
func TestBindMount(t *testing.T) { func TestBindMount(t *testing.T) {
checkSimple(t, "bindMount", []simpleTestCase{ checkSimple(t, "bindMount", []simpleTestCase{
{"mount", func(k syscallDispatcher) error { {"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY) return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)), {"verbosef", expectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil},
}}, stub.UniqueError(0xbad)}, {"mount", expectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, errUnique},
}}, wrapErrSuffix(errUnique, `cannot mount "/host/nix" on "/sysroot/nix":`)},
{"success ne", func(k syscallDispatcher) error { {"success ne", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY) return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY, false)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil), {"verbosef", expectArgs{"resolved %q on %q flags %#x", []any{"/host/nix", "/sysroot/.host-nix", uintptr(1)}}, nil, nil},
call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil), {"mount", expectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil},
{"remount", expectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil},
}}, nil}, }}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY) return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil), {"verbosef", expectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil},
call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil), {"mount", expectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil},
{"remount", expectArgs{"/sysroot/nix", uintptr(1)}, nil, nil},
}}, nil}, }}, nil},
}) })
} }
@ -79,138 +81,138 @@ func TestRemount(t *testing.T) {
checkSimple(t, "remount", []simpleTestCase{ checkSimple(t, "remount", []simpleTestCase{
{"evalSymlinks", func(k syscallDispatcher) error { {"evalSymlinks", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)), {"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", errUnique},
}}, stub.UniqueError(6)}, }}, wrapErrSelf(errUnique)},
{"open", func(k syscallDispatcher) error { {"open", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), {"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, stub.UniqueError(5)), {"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, errUnique},
}}, &os.PathError{Op: "open", Path: "/sysroot/nix", Err: stub.UniqueError(5)}}, }}, wrapErrSuffix(errUnique, `cannot open "/sysroot/nix":`)},
{"readlink", func(k syscallDispatcher) error { {"readlink", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), {"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), {"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", stub.UniqueError(4)), {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", errUnique},
}}, stub.UniqueError(4)}, }}, wrapErrSelf(errUnique)},
{"close", func(k syscallDispatcher) error { {"close", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), {"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), {"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
call("close", stub.ExpectArgs{0xdeadbeef}, nil, stub.UniqueError(3)), {"close", expectArgs{0xdeadbeef}, nil, errUnique},
}}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}}, }}, wrapErrSuffix(errUnique, `cannot close "/sysroot/nix":`)},
{"mountinfo no match", func(k syscallDispatcher) error { {"mountinfo stale", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil), {"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil},
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil), {"verbosef", expectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil},
call("open", stub.ExpectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdeadbeef, nil), {"open", expectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdeadbeef, nil},
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil), {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil},
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), {"close", expectArgs{0xdeadbeef}, nil, nil},
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), {"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
}}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}}, }}, msg.WrapErr(syscall.ESTALE, `mount point "/sysroot/.hakurei" never appeared in mountinfo`)},
{"mountinfo", func(k syscallDispatcher) error { {"mountinfo", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), {"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), {"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), {"close", expectArgs{0xdeadbeef}, nil, nil},
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil), {"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil},
}}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}}, }}, wrapErrSuffix(vfs.ErrMountInfoFields, `cannot parse mountinfo:`)},
{"mount", func(k syscallDispatcher) error { {"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), {"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), {"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), {"close", expectArgs{0xdeadbeef}, nil, nil},
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), {"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, stub.UniqueError(2)), {"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, errUnique},
}}, stub.UniqueError(2)}, }}, wrapErrSuffix(errUnique, `cannot remount "/sysroot/nix":`)},
{"mount propagate", func(k syscallDispatcher) error { {"mount propagate", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), {"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), {"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), {"close", expectArgs{0xdeadbeef}, nil, nil},
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), {"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil), {"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)), {"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, errUnique},
}}, stub.UniqueError(1)}, }}, wrapErrSuffix(errUnique, `cannot propagate flags to "/sysroot/nix/.ro-store":`)},
{"success toplevel", func(k syscallDispatcher) error { {"success toplevel", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil), {"evalSymlinks", expectArgs{"/sysroot/bin"}, "/sysroot/bin", nil},
call("open", stub.ExpectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil), {"open", expectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil},
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/47806"}, "/sysroot/bin", nil), {"readlink", expectArgs{"/host/proc/self/fd/47806"}, "/sysroot/bin", nil},
call("close", stub.ExpectArgs{0xbabe}, nil, nil), {"close", expectArgs{0xbabe}, nil, nil},
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), {"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil), {"mount", expectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil},
}}, nil}, }}, nil},
{"success EACCES", func(k syscallDispatcher) error { {"success EACCES", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), {"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), {"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), {"close", expectArgs{0xdeadbeef}, nil, nil},
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), {"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil), {"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES), {"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil), {"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
}}, nil}, }}, nil},
{"success no propagate", func(k syscallDispatcher) error { {"success no propagate", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), {"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), {"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), {"close", expectArgs{0xdeadbeef}, nil, nil},
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), {"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil), {"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
}}, nil}, }}, nil},
{"success case sensitive", func(k syscallDispatcher) error { {"success case sensitive", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), {"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), {"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), {"close", expectArgs{0xdeadbeef}, nil, nil},
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), {"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil), {"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil), {"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil), {"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
}}, nil}, }}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil), {"evalSymlinks", expectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil},
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil), {"verbosef", expectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil},
call("open", stub.ExpectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdeadbeef, nil), {"open", expectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdeadbeef, nil},
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), {"close", expectArgs{0xdeadbeef}, nil, nil},
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), {"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil), {"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil), {"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil},
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil), {"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
}}, nil}, }}, nil},
}) })
} }
@ -219,18 +221,18 @@ func TestRemountWithFlags(t *testing.T) {
checkSimple(t, "remountWithFlags", []simpleTestCase{ checkSimple(t, "remountWithFlags", []simpleTestCase{
{"noop unmatched", func(k syscallDispatcher) error { {"noop unmatched", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0) return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("verbosef", stub.ExpectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil), {"verbosef", expectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil},
}}, nil}, }}, nil},
{"noop", func(k syscallDispatcher) error { {"noop", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0) return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
}, stub.Expect{}, nil}, }, nil, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY) return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil), {"mount", expectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil},
}}, nil}, }}, nil},
}) })
} }
@ -239,22 +241,22 @@ func TestMountTmpfs(t *testing.T) {
checkSimple(t, "mountTmpfs", []simpleTestCase{ checkSimple(t, "mountTmpfs", []simpleTestCase{
{"mkdirAll", func(k syscallDispatcher) error { {"mkdirAll", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)), {"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, errUnique},
}}, stub.UniqueError(0)}, }}, wrapErrSelf(errUnique)},
{"success no size", func(k syscallDispatcher) error { {"success no size", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil},
call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil), {"mount", expectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil},
}}, nil}, }}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, stub.Expect{Calls: []stub.Call{ }, [][]kexpect{{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil), {"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil},
call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0700,size=1024"}, nil, nil), {"mount", expectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0700,size=1024"}, nil, nil},
}}, nil}, }}, nil},
}) })
} }

View File

@ -2,34 +2,24 @@ package container
import ( import (
"errors" "errors"
"fmt"
"log" "log"
"os"
"reflect"
"sync/atomic" "sync/atomic"
"testing"
) )
// MessageError is an error with a user-facing message.
type MessageError interface {
// Message returns a user-facing error message.
Message() string
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 zeroString, false
}
return e.Message(), true
}
type Msg interface { type Msg interface {
IsVerbose() bool IsVerbose() bool
Verbose(v ...any) Verbose(v ...any)
Verbosef(format string, v ...any) Verbosef(format string, v ...any)
WrapErr(err error, a ...any) error
PrintBaseErr(err error, fallback string)
Suspend() Suspend()
Resume() bool Resume() bool
BeforeExit() BeforeExit()
} }
@ -47,21 +37,32 @@ func (msg *DefaultMsg) Verbosef(format string, v ...any) {
} }
} }
// checkedWrappedErr implements error with strict checks for wrapped values.
type checkedWrappedErr struct {
err error
a []any
}
func (c *checkedWrappedErr) Error() string { return fmt.Sprintf("%v, a = %s", c.err, c.a) }
func (c *checkedWrappedErr) Is(err error) bool {
var concreteErr *checkedWrappedErr
if !errors.As(err, &concreteErr) {
return false
}
return reflect.DeepEqual(c, concreteErr)
}
func (msg *DefaultMsg) WrapErr(err error, a ...any) error {
// provide a mostly bulletproof path to bypass this behaviour in tests
if testing.Testing() && os.Getenv("GOPATH") != Nonexistent {
return &checkedWrappedErr{err, a}
}
log.Println(a...)
return err
}
func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) }
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) } func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) } func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
func (msg *DefaultMsg) BeforeExit() {} func (msg *DefaultMsg) BeforeExit() {}
// msg is the [Msg] implemented used by all exported [container] functions.
var msg Msg = new(DefaultMsg)
// GetOutput returns the current active [Msg] implementation.
func GetOutput() Msg { return msg }
// SetOutput replaces the current active [Msg] implementation.
func SetOutput(v Msg) {
if v == nil {
msg = new(DefaultMsg)
} else {
msg = v
}
}

View File

@ -9,36 +9,13 @@ import (
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/internal/hlog"
) )
func TestMessageError(t *testing.T) {
testCases := []struct {
name string
err error
want string
wantOk bool
}{
{"nil", nil, "", false},
{"new", errors.New(":3"), "", false},
{"start", &container.StartError{
Step: "meow",
Err: syscall.ENOTRECOVERABLE,
}, "cannot meow: state not recoverable", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, ok := container.GetErrorMessage(tc.err)
if got != tc.want {
t.Errorf("GetErrorMessage: %q, want %q", got, tc.want)
}
if ok != tc.wantOk {
t.Errorf("GetErrorMessage: ok = %v, want %v", ok, tc.wantOk)
}
})
}
}
func TestDefaultMsg(t *testing.T) { func TestDefaultMsg(t *testing.T) {
// bypass WrapErr testing behaviour
t.Setenv("GOPATH", container.Nonexistent)
{ {
w := log.Writer() w := log.Writer()
f := log.Flags() f := log.Flags()
@ -71,6 +48,21 @@ func TestDefaultMsg(t *testing.T) {
} }
}) })
t.Run("wrapErr", func(t *testing.T) {
buf := new(strings.Builder)
log.SetOutput(buf)
log.SetFlags(0)
if err := msg.WrapErr(syscall.EBADE, "\x00", "\x00"); err != syscall.EBADE {
t.Errorf("WrapErr: %v", err)
}
msg.PrintBaseErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:")
want := "\x00 \x00\ncannot cuddle cat: state not recoverable\n"
if buf.String() != want {
t.Errorf("WrapErr: %q, want %q", buf.String(), want)
}
})
t.Run("inactive", func(t *testing.T) { t.Run("inactive", func(t *testing.T) {
{ {
inactive := msg.Resume() inactive := msg.Resume()
@ -91,6 +83,25 @@ func TestDefaultMsg(t *testing.T) {
// the function is a noop // the function is a noop
t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() }) t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() })
t.Run("checkedWrappedErr", func(t *testing.T) {
// temporarily re-enable testing behaviour
t.Setenv("GOPATH", "")
wrappedErr := msg.WrapErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:", syscall.ENOTRECOVERABLE)
t.Run("string", func(t *testing.T) {
want := "state not recoverable, a = [cannot cuddle cat: state not recoverable]"
if got := wrappedErr.Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
t.Run("bad concrete type", func(t *testing.T) {
if errors.Is(wrappedErr, syscall.ENOTRECOVERABLE) {
t.Error("incorrect type assertion")
}
})
})
} }
type panicWriter struct{} type panicWriter struct{}
@ -128,6 +139,9 @@ func (out *testOutput) Verbosef(format string, v ...any) {
out.t.Logf(format, v...) out.t.Logf(format, v...)
} }
func (out *testOutput) WrapErr(err error, a ...any) error { return hlog.WrapErr(err, a...) }
func (out *testOutput) PrintBaseErr(err error, fallback string) { hlog.PrintBaseError(err, fallback) }
func (out *testOutput) Suspend() { func (out *testOutput) Suspend() {
if out.suspended.CompareAndSwap(false, true) { if out.suspended.CompareAndSwap(false, true) {
out.Verbose("suspend called") out.Verbose("suspend called")
@ -146,39 +160,3 @@ func (out *testOutput) Resume() bool {
} }
func (out *testOutput) BeforeExit() { out.Verbose("beforeExit called") } func (out *testOutput) BeforeExit() { out.Verbose("beforeExit called") }
func TestGetSetOutput(t *testing.T) {
{
out := container.GetOutput()
t.Cleanup(func() { container.SetOutput(out) })
}
t.Run("default", func(t *testing.T) {
container.SetOutput(new(stubOutput))
if v, ok := container.GetOutput().(*container.DefaultMsg); ok {
t.Fatalf("SetOutput: got unexpected output %#v", v)
}
container.SetOutput(nil)
if _, ok := container.GetOutput().(*container.DefaultMsg); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", container.GetOutput())
}
})
t.Run("stub", func(t *testing.T) {
container.SetOutput(new(stubOutput))
if _, ok := container.GetOutput().(*stubOutput); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", container.GetOutput())
}
})
}
type stubOutput struct {
wrapF func(error, ...any) error
}
func (*stubOutput) IsVerbose() bool { panic("unreachable") }
func (*stubOutput) Verbose(...any) { panic("unreachable") }
func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") }
func (*stubOutput) Suspend() { panic("unreachable") }
func (*stubOutput) Resume() bool { panic("unreachable") }
func (*stubOutput) BeforeExit() { panic("unreachable") }

View File

@ -1,77 +1,26 @@
package container package container
import ( var msg Msg = new(DefaultMsg)
"bytes"
"io"
"sync"
"sync/atomic"
"syscall"
)
const ( func GetOutput() Msg { return msg }
suspendBufInitial = 1 << 12 func SetOutput(v Msg) {
suspendBufMax = 1 << 24 if v == nil {
) msg = new(DefaultMsg)
} else {
// Suspendable proxies writes to a downstream [io.Writer] but optionally withholds writes msg = v
// between calls to Suspend and Resume. }
type Suspendable struct {
Downstream io.Writer
s atomic.Bool
buf bytes.Buffer
// for growing buf
bufOnce sync.Once
// for synchronising all other buf operations
bufMu sync.Mutex
dropped int
} }
func (s *Suspendable) Write(p []byte) (n int, err error) { func wrapErrSuffix(err error, a ...any) error {
if !s.s.Load() { if err == nil {
return s.Downstream.Write(p) return nil
} }
s.bufOnce.Do(func() { s.buf.Grow(suspendBufInitial) }) return msg.WrapErr(err, append(a, err)...)
s.bufMu.Lock()
defer s.bufMu.Unlock()
if free := suspendBufMax - s.buf.Len(); free < len(p) {
// fast path
if free <= 0 {
s.dropped += len(p)
return 0, syscall.ENOMEM
}
n, _ = s.buf.Write(p[:free])
err = syscall.ENOMEM
s.dropped += len(p) - n
return
}
return s.buf.Write(p)
} }
// IsSuspended returns whether [Suspendable] is currently between a call to Suspend and Resume. func wrapErrSelf(err error) error {
func (s *Suspendable) IsSuspended() bool { return s.s.Load() } if err == nil {
return nil
// Suspend causes [Suspendable] to start withholding output in its buffer.
func (s *Suspendable) Suspend() bool { return s.s.CompareAndSwap(false, true) }
// Resume undoes the effect of Suspend and dumps the buffered into the downstream [io.Writer].
func (s *Suspendable) Resume() (resumed bool, dropped uintptr, n int64, err error) {
if s.s.CompareAndSwap(true, false) {
s.bufMu.Lock()
defer s.bufMu.Unlock()
resumed = true
dropped = uintptr(s.dropped)
s.dropped = 0
n, err = io.Copy(s.Downstream, &s.buf)
s.buf.Reset()
} }
return return msg.WrapErr(err, err.Error())
} }

View File

@ -1,155 +1,110 @@
package container_test package container
import ( import (
"bytes"
"errors"
"reflect" "reflect"
"strconv"
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container"
"hakurei.app/container/stub"
) )
func TestSuspendable(t *testing.T) { func TestGetSetOutput(t *testing.T) {
// copied from output.go {
const suspendBufMax = 1 << 24 out := GetOutput()
t.Cleanup(func() { SetOutput(out) })
}
const ( t.Run("default", func(t *testing.T) {
// equivalent to len(want.pt) SetOutput(new(stubOutput))
nSpecialPtEquiv = -iota - 1 if v, ok := GetOutput().(*DefaultMsg); ok {
// equivalent to len(want.w) t.Fatalf("SetOutput: got unexpected output %#v", v)
nSpecialWEquiv }
// suspends writer before executing test case, implies nSpecialWEquiv SetOutput(nil)
nSpecialSuspend if _, ok := GetOutput().(*DefaultMsg); !ok {
// offset: resume writer and measure against dump instead, implies nSpecialPtEquiv t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
nSpecialDump }
) })
t.Run("stub", func(t *testing.T) {
SetOutput(new(stubOutput))
if _, ok := GetOutput().(*stubOutput); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
}
})
}
func TestWrapErr(t *testing.T) {
{
out := GetOutput()
t.Cleanup(func() { SetOutput(out) })
}
var wrapFp *func(error, ...any) error
s := new(stubOutput)
SetOutput(s)
wrapFp = &s.wrapF
// shares the same writer
testCases := []struct { testCases := []struct {
name string name string
w, pt []byte f func(t *testing.T)
err error
wantErr error wantErr error
n int wantA []any
}{ }{
{"simple", []byte{0xde, 0xad, 0xbe, 0xef}, []byte{0xde, 0xad, 0xbe, 0xef}, {"suffix nil", func(t *testing.T) {
nil, nil, nSpecialPtEquiv}, if err := wrapErrSuffix(nil, "\x00"); err != nil {
t.Errorf("wrapErrSuffix: %v", err)
{"error", []byte{0xb, 0xad}, []byte{0xb, 0xad}, }
stub.UniqueError(0), stub.UniqueError(0), nSpecialPtEquiv}, }, nil, nil},
{"suffix val", func(t *testing.T) {
{"suspend short", []byte{0}, nil, if err := wrapErrSuffix(syscall.ENOTRECOVERABLE, "\x00\x00"); err != syscall.ENOTRECOVERABLE {
nil, nil, nSpecialSuspend}, t.Errorf("wrapErrSuffix: %v", err)
{"sw short 0", []byte{0xca, 0xfe, 0xba, 0xbe}, nil, }
nil, nil, nSpecialWEquiv}, }, syscall.ENOTRECOVERABLE, []any{"\x00\x00", syscall.ENOTRECOVERABLE}},
{"sw short 1", []byte{0xff}, nil, {"self nil", func(t *testing.T) {
nil, nil, nSpecialWEquiv}, if err := wrapErrSelf(nil); err != nil {
{"resume short", nil, []byte{0, 0xca, 0xfe, 0xba, 0xbe, 0xff}, nil, nil, t.Errorf("wrapErrSelf: %v", err)
nSpecialDump}, }
}, nil, nil},
{"long pt", bytes.Repeat([]byte{0xff}, suspendBufMax+1), bytes.Repeat([]byte{0xff}, suspendBufMax+1), {"self val", func(t *testing.T) {
nil, nil, nSpecialPtEquiv}, if err := wrapErrSelf(syscall.ENOTRECOVERABLE); err != syscall.ENOTRECOVERABLE {
t.Errorf("wrapErrSelf: %v", err)
{"suspend fill", bytes.Repeat([]byte{0xfe}, suspendBufMax), nil, }
nil, nil, nSpecialSuspend}, }, syscall.ENOTRECOVERABLE, []any{"state not recoverable"}},
{"drop", []byte{0}, nil,
nil, syscall.ENOMEM, 0},
{"drop error", []byte{0}, nil,
stub.UniqueError(1), syscall.ENOMEM, 0},
{"resume fill", nil, bytes.Repeat([]byte{0xfe}, suspendBufMax),
nil, nil, nSpecialDump - 2},
{"suspend fill partial", bytes.Repeat([]byte{0xfd}, suspendBufMax-0xf), nil,
nil, nil, nSpecialSuspend},
{"partial write", bytes.Repeat([]byte{0xad}, 0x1f), nil,
nil, syscall.ENOMEM, 0xf},
{"full drop", []byte{0}, nil,
nil, syscall.ENOMEM, 0},
{"resume fill partial", nil, append(bytes.Repeat([]byte{0xfd}, suspendBufMax-0xf), bytes.Repeat([]byte{0xad}, 0xf)...),
nil, nil, nSpecialDump - 0x10 - 1},
} }
var dw expectWriter
w := container.Suspendable{Downstream: &dw}
for _, tc := range testCases { for _, tc := range testCases {
// these share the same writer, so cannot be subtests t.Run(tc.name, func(t *testing.T) {
t.Logf("writing step %q", tc.name) var (
dw.expect, dw.err = tc.pt, tc.err gotErr error
gotA []any
)
*wrapFp = func(err error, a ...any) error { gotErr = err; gotA = a; return err }
var ( tc.f(t)
gotN int if gotErr != tc.wantErr {
gotErr error t.Errorf("WrapErr: err = %v, want %v", gotErr, tc.wantErr)
)
wantN := tc.n
switch wantN {
case nSpecialPtEquiv:
wantN = len(tc.pt)
gotN, gotErr = w.Write(tc.w)
case nSpecialWEquiv:
wantN = len(tc.w)
gotN, gotErr = w.Write(tc.w)
case nSpecialSuspend:
s := w.IsSuspended()
if ok := w.Suspend(); s && ok {
t.Fatal("Suspend: unexpected success")
} }
wantN = len(tc.w) if !reflect.DeepEqual(gotA, tc.wantA) {
gotN, gotErr = w.Write(tc.w) t.Errorf("WrapErr: a = %v, want %v", gotA, tc.wantA)
default:
if wantN <= nSpecialDump {
if !w.IsSuspended() {
t.Fatal("IsSuspended unexpected false")
}
resumed, dropped, n, err := w.Resume()
if !resumed {
t.Fatal("Resume: resumed = false")
}
if wantDropped := nSpecialDump - wantN; int(dropped) != wantDropped {
t.Errorf("Resume: dropped = %d, want %d", dropped, wantDropped)
}
wantN = len(tc.pt)
gotN, gotErr = int(n), err
} else {
gotN, gotErr = w.Write(tc.w)
} }
} })
if gotN != wantN {
t.Errorf("Write: n = %d, want %d", gotN, wantN)
}
if !reflect.DeepEqual(gotErr, tc.wantErr) {
t.Errorf("Write: %v", gotErr)
}
} }
} }
// expectWriter compares Write calls to expect. type stubOutput struct {
type expectWriter struct { wrapF func(error, ...any) error
expect []byte
err error
} }
func (w *expectWriter) Write(p []byte) (n int, err error) { func (*stubOutput) IsVerbose() bool { panic("unreachable") }
defer func() { w.expect = nil }() func (*stubOutput) Verbose(...any) { panic("unreachable") }
func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") }
func (*stubOutput) PrintBaseErr(error, string) { panic("unreachable") }
func (*stubOutput) Suspend() { panic("unreachable") }
func (*stubOutput) Resume() bool { panic("unreachable") }
func (*stubOutput) BeforeExit() { panic("unreachable") }
n, err = len(p), w.err func (s *stubOutput) WrapErr(err error, v ...any) error {
if w.expect == nil { if s.wrapF == nil {
return 0, errors.New("unexpected call to Write: " + strconv.Quote(string(p))) panic("unreachable")
} }
if string(p) != string(w.expect) { return s.wrapF(err, v...)
return 0, errors.New("p = " + strconv.Quote(string(p)) + ", want " + strconv.Quote(string(w.expect)))
}
return
} }

View File

@ -8,6 +8,11 @@ import (
"syscall" "syscall"
) )
var (
ErrNotSet = errors.New("environment variable not set")
ErrFdFormat = errors.New("bad file descriptor representation")
)
// Setup appends the read end of a pipe for setup params transmission and returns its fd. // Setup appends the read end of a pipe for setup params transmission and returns its fd.
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) { func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
if r, w, err := os.Pipe(); err != nil { if r, w, err := os.Pipe(); err != nil {
@ -19,23 +24,19 @@ func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
} }
} }
var (
ErrReceiveEnv = errors.New("environment variable not set")
)
// Receive retrieves setup fd from the environment and receives params. // Receive retrieves setup fd from the environment and receives params.
func Receive(key string, e any, fdp *uintptr) (func() error, error) { func Receive(key string, e any, fdp *uintptr) (func() error, error) {
var setup *os.File var setup *os.File
if s, ok := os.LookupEnv(key); !ok { if s, ok := os.LookupEnv(key); !ok {
return nil, ErrReceiveEnv return nil, ErrNotSet
} else { } else {
if fd, err := strconv.Atoi(s); err != nil { if fd, err := strconv.Atoi(s); err != nil {
return nil, errors.Unwrap(err) return nil, ErrFdFormat
} else { } else {
setup = os.NewFile(uintptr(fd), "setup") setup = os.NewFile(uintptr(fd), "setup")
if setup == nil { if setup == nil {
return nil, syscall.EDOM return nil, syscall.EBADF
} }
if fdp != nil { if fdp != nil {
*fdp = setup.Fd() *fdp = setup.Fd()

View File

@ -29,8 +29,8 @@ func TestSetupReceive(t *testing.T) {
}) })
} }
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrReceiveEnv) { if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrNotSet) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrReceiveEnv) t.Errorf("Receive: error = %v, want %v", err, container.ErrNotSet)
} }
}) })
@ -38,8 +38,8 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_FORMAT" const key = "TEST_ENV_FORMAT"
t.Setenv(key, "") t.Setenv(key, "")
if _, err := container.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) { if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrFdFormat) {
t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax) t.Errorf("Receive: error = %v, want %v", err, container.ErrFdFormat)
} }
}) })
@ -47,8 +47,8 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_RANGE" const key = "TEST_ENV_RANGE"
t.Setenv(key, "-1") t.Setenv(key, "-1")
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) { if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EBADF) {
t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM) t.Errorf("Receive: error = %v, want %v", err, syscall.EBADF)
} }
}) })

View File

@ -2,6 +2,7 @@ package container
import ( import (
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"os" "os"
"path" "path"
@ -102,29 +103,30 @@ func toHost(name string) string {
func createFile(name string, perm, pperm os.FileMode, content []byte) error { func createFile(name string, perm, pperm os.FileMode, content []byte) error {
if err := os.MkdirAll(path.Dir(name), pperm); err != nil { if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
return err return wrapErrSelf(err)
} }
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm) f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
if err != nil { if err != nil {
return err return wrapErrSelf(err)
} }
if content != nil { if content != nil {
_, err = f.Write(content) _, err = f.Write(content)
} }
return errors.Join(f.Close(), err) return errors.Join(f.Close(), wrapErrSelf(err))
} }
func ensureFile(name string, perm, pperm os.FileMode) error { func ensureFile(name string, perm, pperm os.FileMode) error {
fi, err := os.Stat(name) fi, err := os.Stat(name)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return err return wrapErrSelf(err)
} }
return createFile(name, perm, pperm, nil) return createFile(name, perm, pperm, nil)
} }
if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 { if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
err = &os.PathError{Op: "ensure", Path: name, Err: syscall.EISDIR} err = msg.WrapErr(syscall.EISDIR,
fmt.Sprintf("path %q is a directory", name))
} }
return err return err
} }
@ -145,14 +147,15 @@ func (p *procPaths) stdout() string { return p.self + "/fd/1" }
func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) } func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error { func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
if r, err := p.k.openNew(p.self + "/mountinfo"); err != nil { if r, err := p.k.openNew(p.self + "/mountinfo"); err != nil {
return err return wrapErrSelf(err)
} else { } else {
d := vfs.NewMountInfoDecoder(r) d := vfs.NewMountInfoDecoder(r)
err0 := f(d) err0 := f(d)
if err = r.Close(); err != nil { if err = r.Close(); err != nil {
return err return wrapErrSelf(err)
} else if err = d.Err(); err != nil { } else if err = d.Err(); err != nil {
return err return wrapErrSuffix(err,
"cannot parse mountinfo:")
} }
return err0 return err0
} }

View File

@ -1,6 +1,8 @@
package container package container
import ( import (
"errors"
"fmt"
"io" "io"
"math" "math"
"os" "os"
@ -54,27 +56,20 @@ func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment(
func TestCreateFile(t *testing.T) { func TestCreateFile(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) { t.Run("nonexistent", func(t *testing.T) {
t.Run("mkdir", func(t *testing.T) { if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{
wantErr := &os.PathError{ Op: "mkdir",
Op: "mkdir", Path: "/proc/nonexistent",
Path: "/proc/nonexistent", Err: syscall.ENOENT,
Err: syscall.ENOENT, })) {
} t.Errorf("createFile: error = %v", err)
if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) { }
t.Errorf("createFile: error = %#v, want %#v", err, wantErr) if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{
} Op: "open",
}) Path: "/proc/nonexistent",
Err: syscall.ENOENT,
t.Run("open", func(t *testing.T) { })) {
wantErr := &os.PathError{ t.Errorf("createFile: error = %v", err)
Op: "open", }
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
}
if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
}
})
}) })
t.Run("touch", func(t *testing.T) { t.Run("touch", func(t *testing.T) {
@ -125,13 +120,13 @@ func TestEnsureFile(t *testing.T) {
t.Fatalf("Chmod: error = %v", err) t.Fatalf("Chmod: error = %v", err)
} }
wantErr := &os.PathError{ wantErr := wrapErrSelf(&os.PathError{
Op: "stat", Op: "stat",
Path: pathname, Path: pathname,
Err: syscall.EACCES, Err: syscall.EACCES,
} })
if err := ensureFile(pathname, 0644, 0755); !reflect.DeepEqual(err, wantErr) { if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) {
t.Errorf("ensureFile: error = %#v, want %#v", err, wantErr) t.Errorf("ensureFile: error = %v, want %v", err, wantErr)
} }
if err := os.Chmod(tempDir, 0755); err != nil { if err := os.Chmod(tempDir, 0755); err != nil {
@ -141,9 +136,9 @@ func TestEnsureFile(t *testing.T) {
t.Run("directory", func(t *testing.T) { t.Run("directory", func(t *testing.T) {
pathname := t.TempDir() pathname := t.TempDir()
wantErr := &os.PathError{Op: "ensure", Path: pathname, Err: syscall.EISDIR} wantErr := msg.WrapErr(syscall.EISDIR, fmt.Sprintf("path %q is a directory", pathname))
if err := ensureFile(pathname, 0644, 0755); !reflect.DeepEqual(err, wantErr) { if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) {
t.Errorf("ensureFile: error = %#v, want %#v", err, wantErr) t.Errorf("ensureFile: error = %v, want %v", err, wantErr)
} }
}) })
@ -182,12 +177,12 @@ func TestProcPaths(t *testing.T) {
t.Run("mountinfo", func(t *testing.T) { t.Run("mountinfo", func(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) { t.Run("nonexistent", func(t *testing.T) {
nonexistentProc := newProcPaths(direct{}, t.TempDir()) nonexistentProc := newProcPaths(direct{}, t.TempDir())
wantErr := &os.PathError{ wantErr := wrapErrSelf(&os.PathError{
Op: "open", Op: "open",
Path: nonexistentProc.self + "/mountinfo", Path: nonexistentProc.self + "/mountinfo",
Err: syscall.ENOENT, Err: syscall.ENOENT,
} })
if err := nonexistentProc.mountinfo(func(*vfs.MountInfoDecoder) error { return syscall.EINVAL }); !reflect.DeepEqual(err, wantErr) { if err := nonexistentProc.mountinfo(func(*vfs.MountInfoDecoder) error { return syscall.EINVAL }); !errors.Is(err, wantErr) {
t.Errorf("mountinfo: error = %v, want %v", err, wantErr) t.Errorf("mountinfo: error = %v, want %v", err, wantErr)
} }
}) })
@ -222,11 +217,11 @@ func TestProcPaths(t *testing.T) {
t.Run("closed", func(t *testing.T) { t.Run("closed", func(t *testing.T) {
p := newProcPaths(direct{}, tempDir) p := newProcPaths(direct{}, tempDir)
wantErr := &os.PathError{ wantErr := wrapErrSelf(&os.PathError{
Op: "close", Op: "close",
Path: p.self + "/mountinfo", Path: p.self + "/mountinfo",
Err: os.ErrClosed, Err: os.ErrClosed,
} })
if err := p.mountinfo(func(d *vfs.MountInfoDecoder) error { if err := p.mountinfo(func(d *vfs.MountInfoDecoder) error {
v := reflect.ValueOf(d).Elem().FieldByName("s").Elem().FieldByName("r") v := reflect.ValueOf(d).Elem().FieldByName("s").Elem().FieldByName("r")
v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())) v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr()))
@ -236,8 +231,8 @@ func TestProcPaths(t *testing.T) {
} else { } else {
return f.Close() return f.Close()
} }
}); !reflect.DeepEqual(err, wantErr) { }); !errors.Is(err, wantErr) {
t.Errorf("mountinfo: error = %#v, want %#v", err, wantErr) t.Errorf("mountinfo: error = %v, want %v", err, wantErr)
} }
}) })
@ -247,8 +242,8 @@ func TestProcPaths(t *testing.T) {
t.Fatalf("WriteFile: error = %v", err) t.Fatalf("WriteFile: error = %v", err)
} }
wantErr := &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields} wantErr := wrapErrSuffix(vfs.ErrMountInfoFields, "cannot parse mountinfo:")
if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !reflect.DeepEqual(err, wantErr) { if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !errors.Is(err, wantErr) {
t.Fatalf("mountinfo: error = %v, want %v", err, wantErr) t.Fatalf("mountinfo: error = %v, want %v", err, wantErr)
} }
}) })

View File

@ -1,37 +0,0 @@
package stub
import (
"slices"
)
// ExpectArgs is an array primarily for storing expected function arguments.
// Its actual use is defined by the implementation.
type ExpectArgs = [5]any
// An Expect stores expected calls of a goroutine.
type Expect struct {
Calls []Call
// Tracks are handed out to descendant goroutines in order.
Tracks []Expect
}
// A Call holds expected arguments of a function call and its outcome.
type Call struct {
// Name is the function Name of this call. Must be unique.
Name string
// Args are the expected arguments of this Call.
Args ExpectArgs
// Ret is the return value of this Call.
Ret any
// Err is the returned error of this Call.
Err error
}
// Error returns [Call.Err] if all arguments are true, or [ErrCheck] otherwise.
func (k *Call) Error(ok ...bool) error {
if !slices.Contains(ok, false) {
return k.Err
}
return ErrCheck
}

View File

@ -1,23 +0,0 @@
package stub_test
import (
"reflect"
"testing"
"hakurei.app/container/stub"
)
func TestCallError(t *testing.T) {
t.Run("contains false", func(t *testing.T) {
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) {
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

@ -1,25 +0,0 @@
package stub
import (
"errors"
"strconv"
)
var (
ErrCheck = errors.New("one or more arguments did not match")
)
// UniqueError is an error that only equivalates to other [UniqueError] with the same magic value.
type UniqueError uintptr
func (e UniqueError) Error() string {
return "unique error " + strconv.Itoa(int(e)) + " injected by the test suite"
}
func (e UniqueError) Is(target error) bool {
var u UniqueError
if !errors.As(target, &u) {
return false
}
return e == u
}

View File

@ -1,35 +0,0 @@
package stub_test
import (
"errors"
"syscall"
"testing"
"hakurei.app/container/stub"
)
func TestUniqueError(t *testing.T) {
t.Run("format", func(t *testing.T) {
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)
}
})
t.Run("is", func(t *testing.T) {
t.Run("type", func(t *testing.T) {
if errors.Is(stub.UniqueError(0), syscall.ENOTRECOVERABLE) {
t.Error("Is: unexpected true")
}
})
t.Run("val", func(t *testing.T) {
if errors.Is(stub.UniqueError(0), stub.UniqueError(1)) {
t.Error("Is: unexpected true")
}
if !errors.Is(stub.UniqueError(0xbad), stub.UniqueError(0xbad)) {
t.Error("Is: unexpected false")
}
})
})
}

View File

@ -1,44 +0,0 @@
package stub
import "testing"
// PanicExit is a magic panic value treated as a simulated exit.
const PanicExit = 0xdeadbeef
const (
panicFailNow = 0xcafe0000 + iota
panicFatal
panicFatalf
)
// HandleExit must be deferred before calling with the stub.
func HandleExit(t testing.TB) {
switch r := recover(); r {
case PanicExit:
break
case panicFailNow:
t.FailNow()
case panicFatal, panicFatalf, nil:
break
default:
panic(r)
}
}
// handleExitNew handles exits from goroutines created by [Stub.New].
func handleExitNew(t testing.TB) {
switch r := recover(); r {
case PanicExit, panicFatal, panicFatalf, nil:
break
case panicFailNow:
t.Fail()
break
default:
panic(r)
}
}

View File

@ -1,93 +0,0 @@
package stub_test
import (
"testing"
_ "unsafe"
"hakurei.app/container/stub"
)
//go:linkname handleExitNew hakurei.app/container/stub.handleExitNew
func handleExitNew(_ testing.TB)
// overrideTFailNow overrides the Fail and FailNow method.
type overrideTFailNow struct {
*testing.T
failNow bool
fail bool
}
func (o *overrideTFailNow) FailNow() {
if o.failNow {
o.Errorf("attempted to FailNow twice")
}
o.failNow = true
}
func (o *overrideTFailNow) Fail() {
if o.fail {
o.Errorf("attempted to Fail twice")
}
o.fail = true
}
func TestHandleExit(t *testing.T) {
t.Run("exit", func(t *testing.T) {
defer stub.HandleExit(t)
panic(stub.PanicExit)
})
t.Run("goexit", func(t *testing.T) {
t.Run("FailNow", func(t *testing.T) {
ot := &overrideTFailNow{T: t}
defer func() {
if !ot.failNow {
t.Errorf("FailNow was never called")
}
}()
defer stub.HandleExit(ot)
panic(0xcafe0000)
})
t.Run("Fail", func(t *testing.T) {
ot := &overrideTFailNow{T: t}
defer func() {
if !ot.fail {
t.Errorf("Fail was never called")
}
}()
defer handleExitNew(ot)
panic(0xcafe0000)
})
})
t.Run("nil", func(t *testing.T) {
defer stub.HandleExit(t)
})
t.Run("passthrough", func(t *testing.T) {
t.Run("toplevel", func(t *testing.T) {
defer func() {
want := 0xcafebabe
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
defer stub.HandleExit(t)
panic(0xcafebabe)
})
t.Run("new", func(t *testing.T) {
defer func() {
want := 0xcafe
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
defer handleExitNew(t)
panic(0xcafe)
})
})
}

View File

@ -1,148 +0,0 @@
// Package stub provides function call level stubbing and validation
// for library functions that are impossible to check otherwise.
package stub
import (
"reflect"
"sync"
"testing"
)
// this should prevent stub from being inadvertently imported outside tests
var _ = func() {
if !testing.Testing() {
panic("stub imported while not in a test")
}
}
const (
// A CallSeparator denotes an injected separation between two groups of calls.
CallSeparator = "\x00"
)
// A Stub is a collection of tracks of expected calls.
type Stub[K any] struct {
testing.TB
// makeK creates a new K for a descendant [Stub].
// This function may be called concurrently.
makeK func(s *Stub[K]) K
// want is a hierarchy of expected calls.
want Expect
// pos is the current position in [Expect.Calls].
pos int
// goroutine counts the number of goroutines created by this [Stub].
goroutine int
// sub stores the addresses of descendant [Stub] created by New.
sub []*Stub[K]
// wg waits for all descendants to complete.
wg *sync.WaitGroup
}
// New creates a root [Stub].
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() { 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)) {
s.Helper()
s.Expects("New")
if len(s.want.Tracks) <= s.goroutine {
s.Fatal("New: track overrun")
}
ds := &Stub[K]{TB: s.TB, makeK: s.makeK, want: s.want.Tracks[s.goroutine], wg: s.wg}
s.goroutine++
s.sub = append(s.sub, ds)
s.wg.Add(1)
go func() {
s.Helper()
defer s.wg.Done()
defer handleExitNew(s.TB)
f(s.makeK(ds))
}()
}
// Pos returns the current position of [Stub] in its [Expect.Calls]
func (s *Stub[K]) Pos() int { return s.pos }
// Len returns the length of [Expect.Calls].
func (s *Stub[K]) Len() int { return len(s.want.Calls) }
// VisitIncomplete calls f on an incomplete s and all its descendants.
func (s *Stub[K]) VisitIncomplete(f func(s *Stub[K])) {
s.Helper()
s.wg.Wait()
if s.want.Calls != nil && len(s.want.Calls) != s.pos {
f(s)
}
for _, ds := range s.sub {
ds.VisitIncomplete(f)
}
}
// Expects checks the name of and returns the current [Call] and advances pos.
func (s *Stub[K]) Expects(name string) (expect *Call) {
s.Helper()
if len(s.want.Calls) == s.pos {
s.Fatal("Expects: advancing beyond expected calls")
}
expect = &s.want.Calls[s.pos]
if name != expect.Name {
if expect.Name == CallSeparator {
s.Fatalf("Expects: func = %s, separator overrun", name)
}
if name == CallSeparator {
s.Fatalf("Expects: separator, want %s", expect.Name)
}
s.Fatalf("Expects: func = %s, want %s", name, expect.Name)
}
s.pos++
return
}
// CheckArg checks an argument comparable with the == operator. Avoid using this with pointers.
func CheckArg[T comparable, K any](s *Stub[K], arg string, got T, n int) bool {
s.Helper()
pos := s.pos - 1
if pos < 0 || pos >= len(s.want.Calls) {
panic("invalid call to CheckArg")
}
expect := s.want.Calls[pos]
want, ok := expect.Args[n].(T)
if !ok || got != want {
s.Errorf("%s: %s = %#v, want %#v (%d)", expect.Name, arg, got, want, pos)
return false
}
return true
}
// CheckArgReflect checks an argument of any type.
func CheckArgReflect[K any](s *Stub[K], arg string, got any, n int) bool {
s.Helper()
pos := s.pos - 1
if pos < 0 || pos >= len(s.want.Calls) {
panic("invalid call to CheckArgReflect")
}
expect := s.want.Calls[pos]
want := expect.Args[n]
if !reflect.DeepEqual(got, want) {
s.Errorf("%s: %s = %#v, want %#v (%d)", expect.Name, arg, got, want, pos)
return false
}
return true
}

View File

@ -1,296 +0,0 @@
package stub
import (
"reflect"
"sync/atomic"
"testing"
)
// stubHolder embeds [Stub].
type stubHolder struct{ *Stub[stubHolder] }
// overrideT allows some methods of [testing.T] to be overridden.
type overrideT struct {
*testing.T
error atomic.Pointer[func(args ...any)]
errorf atomic.Pointer[func(format string, args ...any)]
}
func (t *overrideT) Error(args ...any) {
fp := t.error.Load()
if fp == nil || *fp == nil {
t.T.Error(args...)
return
}
(*fp)(args...)
}
func (t *overrideT) Errorf(format string, args ...any) {
fp := t.errorf.Load()
if fp == nil || *fp == nil {
t.T.Errorf(format, args...)
return
}
(*fp)(format, args...)
}
func TestStub(t *testing.T) {
t.Run("goexit", func(t *testing.T) {
t.Run("FailNow", func(t *testing.T) {
defer func() {
if r := recover(); r != panicFailNow {
t.Errorf("recover: %v", r)
}
}()
new(stubHolder).FailNow()
})
t.Run("SkipNow", func(t *testing.T) {
defer func() {
want := "invalid call to SkipNow"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
new(stubHolder).SkipNow()
})
t.Run("Skip", func(t *testing.T) {
defer func() {
want := "invalid call to Skip"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
new(stubHolder).Skip()
})
t.Run("Skipf", func(t *testing.T) {
defer func() {
want := "invalid call to Skipf"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
new(stubHolder).Skipf("")
})
})
t.Run("new", func(t *testing.T) {
t.Run("success", func(t *testing.T) {
s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
{"New", ExpectArgs{}, nil, nil},
}, Tracks: []Expect{{Calls: []Call{
{"done", ExpectArgs{0xbabe}, nil, nil},
}}}})
s.New(func(k stubHolder) {
expect := k.Expects("done")
if expect.Name != "done" {
t.Errorf("New: Name = %s, want done", expect.Name)
}
if expect.Args != (ExpectArgs{0xbabe}) {
t.Errorf("New: Args = %#v", expect.Args)
}
if expect.Ret != nil {
t.Errorf("New: Ret = %#v", expect.Ret)
}
if expect.Err != nil {
t.Errorf("New: Err = %#v", expect.Err)
}
})
if pos := s.Pos(); pos != 1 {
t.Errorf("Pos: %d, want 1", pos)
}
if l := s.Len(); l != 1 {
t.Errorf("Len: %d, want 1", l)
}
s.VisitIncomplete(func(s *Stub[stubHolder]) { panic("unreachable") })
})
t.Run("overrun", func(t *testing.T) {
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{
{"New", ExpectArgs{}, nil, nil},
{"panic", ExpectArgs{"unreachable"}, nil, nil},
}})
func() { defer HandleExit(t); s.New(func(k stubHolder) { panic("unreachable") }) }()
var visit int
s.VisitIncomplete(func(s *Stub[stubHolder]) {
visit++
if visit > 1 {
panic("unexpected visit count")
}
want := Call{"panic", ExpectArgs{"unreachable"}, nil, nil}
if got := s.want.Calls[s.pos]; !reflect.DeepEqual(got, want) {
t.Errorf("VisitIncomplete: %#v, want %#v", got, want)
}
})
})
t.Run("expects", func(t *testing.T) {
t.Run("overrun", func(t *testing.T) {
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{})
func() { defer HandleExit(t); s.Expects("unreachable") }()
})
t.Run("separator", func(t *testing.T) {
t.Run("overrun", func(t *testing.T) {
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{
{CallSeparator, ExpectArgs{}, nil, nil},
}})
func() { defer HandleExit(t); s.Expects("meow") }()
})
t.Run("mismatch", func(t *testing.T) {
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{
{"panic", ExpectArgs{}, nil, nil},
}})
func() { defer HandleExit(t); s.Expects(CallSeparator) }()
})
})
t.Run("mismatch", func(t *testing.T) {
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{
{"nya", ExpectArgs{}, nil, nil},
}})
func() { defer HandleExit(t); s.Expects("meow") }()
})
})
})
}
func TestCheckArg(t *testing.T) {
t.Run("oob negative", func(t *testing.T) {
defer func() {
want := "invalid call to CheckArg"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
CheckArg(s, "unreachable", struct{}{}, 0)
})
ot := &overrideT{T: t}
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
{"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")
ot.errorf.Store(checkErrorf(t, "%s: %s = %#v, want %#v (%d)", "meow", "time", 0, -1, 1))
if CheckArg(s, "time", 0, 0) {
t.Errorf("CheckArg: unexpected true")
}
})
t.Run("oob", func(t *testing.T) {
s.pos++
defer func() {
want := "invalid call to CheckArg"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
CheckArg(s, "unreachable", struct{}{}, 0)
})
}
func TestCheckArgReflect(t *testing.T) {
t.Run("oob lower", func(t *testing.T) {
defer func() {
want := "invalid call to CheckArgReflect"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
CheckArgReflect(s, "unreachable", struct{}{}, 0)
})
ot := &overrideT{T: t}
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
{"panic", ExpectArgs{PanicExit}, nil, nil},
{"meow", ExpectArgs{-1}, nil, nil},
}})
t.Run("match", func(t *testing.T) {
s.Expects("panic")
if !CheckArgReflect(s, "v", PanicExit, 0) {
t.Errorf("CheckArgReflect: unexpected false")
}
})
t.Run("mismatch", func(t *testing.T) {
defer HandleExit(t)
s.Expects("meow")
ot.errorf.Store(checkErrorf(t, "%s: %s = %#v, want %#v (%d)", "meow", "time", 0, -1, 1))
if CheckArgReflect(s, "time", 0, 0) {
t.Errorf("CheckArgReflect: unexpected true")
}
})
t.Run("oob", func(t *testing.T) {
s.pos++
defer func() {
want := "invalid call to CheckArgReflect"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
CheckArgReflect(s, "unreachable", struct{}{}, 0)
})
}
func checkError(t *testing.T, wantArgs ...any) *func(args ...any) {
var called bool
f := func(args ...any) {
if called {
panic("invalid call to error")
}
called = true
if !reflect.DeepEqual(args, wantArgs) {
t.Errorf("Error: %#v, want %#v", args, wantArgs)
}
panic(PanicExit)
}
return &f
}
func checkErrorf(t *testing.T, wantFormat string, wantArgs ...any) *func(format string, args ...any) {
var called bool
f := func(format string, args ...any) {
if called {
panic("invalid call to errorf")
}
called = true
if format != wantFormat {
t.Errorf("Errorf: format = %q, want %q", format, wantFormat)
}
if !reflect.DeepEqual(args, wantArgs) {
t.Errorf("Errorf: args = %#v, want %#v", args, wantArgs)
}
panic(PanicExit)
}
return &f
}

View File

@ -24,32 +24,6 @@ var (
ErrMountInfoSep = errors.New("bad optional fields separator") ErrMountInfoSep = errors.New("bad optional fields separator")
) )
type DecoderError struct {
Op string
Line int
Err error
}
func (e *DecoderError) Unwrap() error { return e.Err }
func (e *DecoderError) Error() string {
var s string
var numError *strconv.NumError
switch {
case errors.As(e.Err, &numError) && numError != nil:
s = "numeric field " + strconv.Quote(numError.Num) + " " + numError.Err.Error()
default:
s = e.Err.Error()
}
var atLine string
if e.Line >= 0 {
atLine = " at line " + strconv.Itoa(e.Line)
}
return e.Op + " mountinfo" + atLine + ": " + s
}
type ( type (
// A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream. // A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream.
MountInfoDecoder struct { MountInfoDecoder struct {
@ -58,7 +32,6 @@ type (
current *MountInfo current *MountInfo
parseErr error parseErr error
curLine int
complete bool complete bool
} }
@ -159,12 +132,9 @@ func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] {
func (d *MountInfoDecoder) Err() error { func (d *MountInfoDecoder) Err() error {
if err := d.s.Err(); err != nil { if err := d.s.Err(); err != nil {
return &DecoderError{"scan", d.curLine, err} return err
} }
if d.parseErr != nil { return d.parseErr
return &DecoderError{"parse", d.curLine, d.parseErr}
}
return nil
} }
func (d *MountInfoDecoder) scan() bool { func (d *MountInfoDecoder) scan() bool {
@ -190,7 +160,6 @@ func (d *MountInfoDecoder) scan() bool {
d.current.Next = m d.current.Next = m
d.current = d.current.Next d.current = d.current.Next
} }
d.curLine++
return true return true
} }

View File

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"iter" "iter"
"os"
"path" "path"
"reflect" "reflect"
"slices" "slices"
@ -16,102 +15,62 @@ import (
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
) )
func TestDecoderError(t *testing.T) {
testCases := []struct {
name string
err *vfs.DecoderError
want string
target error
targetF error
}{
{"errno", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: syscall.ENOTRECOVERABLE},
"parse mountinfo at line 3735928559: state not recoverable", syscall.ENOTRECOVERABLE, syscall.EROFS},
{"strconv", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
`parse mountinfo at line 3735928559: numeric field "meow" invalid syntax`, strconv.ErrSyntax, os.ErrInvalid},
{"unfold", &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/proc/nonexistent")},
"unfold mountinfo: mount point /proc/nonexistent never appeared in mountinfo", vfs.UnfoldTargetError("/proc/nonexistent"), os.ErrNotExist},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %s, want %s", got, tc.want)
}
})
t.Run("is", func(t *testing.T) {
if !errors.Is(tc.err, tc.target) {
t.Errorf("Is: unexpected false")
}
if errors.Is(tc.err, tc.targetF) {
t.Errorf("Is: unexpected true")
}
})
})
}
}
func TestMountInfo(t *testing.T) { func TestMountInfo(t *testing.T) {
testCases := []mountInfoTest{ testCases := []mountInfoTest{
{"count", sampleMountinfoBase + ` {"count", sampleMountinfoBase + `
21 20 0:53/ /mnt/test rw,relatime - tmpfs rw 21 20 0:53/ /mnt/test rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoFields}, vfs.ErrMountInfoFields, "", nil, nil, nil},
"", nil, nil, nil},
{"sep", sampleMountinfoBase + ` {"sep", sampleMountinfoBase + `
21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw 21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoSep}, vfs.ErrMountInfoSep, "", nil, nil, nil},
"", nil, nil, nil},
{"id", sampleMountinfoBase + ` {"id", sampleMountinfoBase + `
id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
&vfs.DecoderError{Op: "parse", Line: 6, Err: &strconv.NumError{Func: "Atoi", Num: "id", Err: strconv.ErrSyntax}}, strconv.ErrSyntax, "", nil, nil, nil},
"", nil, nil, nil},
{"parent", sampleMountinfoBase + ` {"parent", sampleMountinfoBase + `
21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
&vfs.DecoderError{Op: "parse", Line: 6, Err: &strconv.NumError{Func: "Atoi", Num: "parent", Err: strconv.ErrSyntax}}, "", nil, nil, nil}, strconv.ErrSyntax, "", nil, nil, nil},
{"devno", sampleMountinfoBase + ` {"devno", sampleMountinfoBase + `
21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw 21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "parse mountinfo at line 6: unexpected EOF", nil, nil, nil}, nil, "unexpected EOF", nil, nil, nil},
{"maj", sampleMountinfoBase + ` {"maj", sampleMountinfoBase + `
21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "parse mountinfo at line 6: expected integer", nil, nil, nil}, nil, "expected integer", nil, nil, nil},
{"min", sampleMountinfoBase + ` {"min", sampleMountinfoBase + `
21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw 21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "parse mountinfo at line 6: expected integer", nil, nil, nil}, nil, "expected integer", nil, nil, nil},
{"mountroot", sampleMountinfoBase + ` {"mountroot", sampleMountinfoBase + `
21 20 0:53 /mnt/test rw,relatime - tmpfs rw 21 20 0:53 /mnt/test rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil}, vfs.ErrMountInfoEmpty, "", nil, nil, nil},
{"target", sampleMountinfoBase + ` {"target", sampleMountinfoBase + `
21 20 0:53 / rw,relatime - tmpfs rw 21 20 0:53 / rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil}, vfs.ErrMountInfoEmpty, "", nil, nil, nil},
{"vfs options", sampleMountinfoBase + ` {"vfs options", sampleMountinfoBase + `
21 20 0:53 / /mnt/test - tmpfs rw 21 20 0:53 / /mnt/test - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil}, vfs.ErrMountInfoEmpty, "", nil, nil, nil},
{"FS type", sampleMountinfoBase + ` {"FS type", sampleMountinfoBase + `
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 21 20 0:53 / /mnt/test rw,relatime - rw
21 20 0:53 / /mnt/test rw,relatime - rw`, 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
&vfs.DecoderError{Op: "parse", Line: 7, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil}, vfs.ErrMountInfoEmpty, "", nil, nil, nil},
{"base", sampleMountinfoBase, nil, "", []*wantMountInfo{ {"base", sampleMountinfoBase, nil, "", []*wantMountInfo{
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil), m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
@ -307,9 +266,9 @@ func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName s
}) })
} else if tc.wantNode != nil || tc.wantCollectF != nil { } else if tc.wantNode != nil || tc.wantCollectF != nil {
panic("invalid test case") panic("invalid test case")
} else if _, err := d.Unfold("/"); !reflect.DeepEqual(err, tc.wantErr) { } else if _, err := d.Unfold("/"); !errors.Is(err, tc.wantErr) {
if tc.wantError == "" { if tc.wantError == "" {
t.Errorf("Unfold: error = %#v, wantErr %#v", t.Errorf("Unfold: error = %v, wantErr %v",
err, tc.wantErr) err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError { } else if err != nil && err.Error() != tc.wantError {
t.Errorf("Unfold: error = %q, wantError %q", t.Errorf("Unfold: error = %q, wantError %q",
@ -317,9 +276,9 @@ func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName s
} }
} }
if err := gotErr(); !reflect.DeepEqual(err, tc.wantErr) { if err := gotErr(); !errors.Is(err, tc.wantErr) {
if tc.wantError == "" { if tc.wantError == "" {
t.Errorf("%s: error = %#v, wantErr %#v", t.Errorf("%s: error = %v, wantErr %v",
funcName, err, tc.wantErr) funcName, err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError { } else if err != nil && err.Error() != tc.wantError {
t.Errorf("%s: error = %q, wantError %q", t.Errorf("%s: error = %q, wantError %q",

View File

@ -4,14 +4,9 @@ import (
"iter" "iter"
"path" "path"
"strings" "strings"
"syscall"
) )
type UnfoldTargetError string
func (e UnfoldTargetError) Error() string {
return "mount point " + string(e) + " never appeared in mountinfo"
}
// MountInfoNode positions a [MountInfoEntry] in its mount hierarchy. // MountInfoNode positions a [MountInfoEntry] in its mount hierarchy.
type MountInfoNode struct { type MountInfoNode struct {
*MountInfoEntry *MountInfoEntry
@ -70,8 +65,7 @@ func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) {
} }
if targetIndex == -1 { if targetIndex == -1 {
// target does not exist in parsed mountinfo return nil, syscall.ESTALE
return nil, &DecoderError{Op: "unfold", Line: -1, Err: UnfoldTargetError(targetClean)}
} }
for _, cur := range mountinfo { for _, cur := range mountinfo {

View File

@ -1,9 +1,11 @@
package vfs_test package vfs_test
import ( import (
"errors"
"reflect" "reflect"
"slices" "slices"
"strings" "strings"
"syscall"
"testing" "testing"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
@ -24,7 +26,7 @@ func TestUnfold(t *testing.T) {
"no match", "no match",
sampleMountinfoBase, sampleMountinfoBase,
"/mnt", "/mnt",
&vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/mnt")}, nil, nil, nil, syscall.ESTALE, nil, nil, nil,
}, },
{ {
"cover", "cover",
@ -53,7 +55,7 @@ func TestUnfold(t *testing.T) {
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
got, err := d.Unfold(tc.target) got, err := d.Unfold(tc.target)
if !reflect.DeepEqual(err, tc.wantErr) { if !errors.Is(err, tc.wantErr) {
t.Errorf("Unfold: error = %v, wantErr %v", t.Errorf("Unfold: error = %v, wantErr %v",
err, tc.wantErr) err, tc.wantErr)
} }

12
flake.lock generated
View File

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1756679287, "lastModified": 1753479839,
"narHash": "sha256-Xd1vOeY9ccDf5VtVK12yM0FS6qqvfUop8UQlxEB+gTQ=", "narHash": "sha256-E/rPVh7vyPMJUFl2NAew+zibNGfVbANr8BP8nLRbLkQ=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "07fc025fe10487dd80f2ec694f1cd790e752d0e8", "rev": "0b9bf983db4d064764084cd6748efb1ab8297d1e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -23,11 +23,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1757020766, "lastModified": 1753345091,
"narHash": "sha256-PLoSjHRa2bUbi1x9HoXgTx2AiuzNXs54c8omhadyvp0=", "narHash": "sha256-CdX2Rtvp5I8HGu9swBmYuq+ILwRxpXdJwlpg8jvN4tU=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "fe83bbdde2ccdc2cb9573aa846abe8363f79a97a", "rev": "3ff0e34b1383648053bba8ed03f201d3466f90c9",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -11,10 +11,10 @@ import (
) )
func TestContainer(t *testing.T) { func TestContainer(t *testing.T) {
t.Run("start invalid container", func(t *testing.T) { t.Run("start empty container", func(t *testing.T) {
h := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil) h := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil)
wantErr := "container: starting an invalid container" wantErr := "container: starting an empty container"
if err := h.Start(); err == nil || err.Error() != wantErr { if err := h.Start(); err == nil || err.Error() != wantErr {
t.Errorf("Start: error = %v, wantErr %q", t.Errorf("Start: error = %v, wantErr %q",
err, wantErr) err, wantErr)

View File

@ -6,10 +6,8 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"reflect"
"strconv" "strconv"
"strings" "strings"
"syscall"
"testing" "testing"
"time" "time"
@ -49,10 +47,6 @@ func argFChecked(argsFd, statFd int) (args []string) {
return return
} }
const (
containerTimeout = 30 * time.Second
)
// this function tests an implementation of the helper.Helper interface // this function tests an implementation of the helper.Helper interface
func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper) { func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper) {
oldWaitDelay := helper.WaitDelay oldWaitDelay := helper.WaitDelay
@ -60,15 +54,18 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay }) t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
t.Run("start helper with status channel and wait", func(t *testing.T) { t.Run("start helper with status channel and wait", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), containerTimeout) ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
stdout := new(strings.Builder) stdout := new(strings.Builder)
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, true) h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, true)
t.Run("wait not yet started helper", func(t *testing.T) { t.Run("wait not yet started helper", func(t *testing.T) {
if err := h.Wait(); !reflect.DeepEqual(err, syscall.EINVAL) && defer func() {
!reflect.DeepEqual(err, errors.New("exec: not started")) { r := recover()
t.Errorf("Wait: error = %v", err) if r == nil {
} t.Fatalf("Wait did not panic")
}
}()
panic(fmt.Sprintf("unreachable: %v", h.Wait()))
}) })
t.Log("starting helper stub") t.Log("starting helper stub")
@ -111,7 +108,7 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
}) })
t.Run("start helper and wait", func(t *testing.T) { t.Run("start helper and wait", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), containerTimeout) ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel() defer cancel()
stdout := new(strings.Builder) stdout := new(strings.Builder)
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false) h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false)

View File

@ -87,9 +87,7 @@ type (
// initial process environment variables // initial process environment variables
Env map[string]string `json:"env"` Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace; // map target user uid to privileged user uid in the user namespace
// some programs fail to connect to dbus session running as a different uid,
// this option works around it by mapping priv-side caller uid in container
MapRealUID bool `json:"map_real_uid"` MapRealUID bool `json:"map_real_uid"`
// pass through all devices // pass through all devices

View File

@ -2,42 +2,12 @@
package hst package hst
import ( import (
"errors"
"net"
"os"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/system" "hakurei.app/system"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
// An AppError is returned while starting an app according to [hst.Config].
type AppError struct {
Step string
Err error
Msg string
}
func (e *AppError) Error() string { return e.Err.Error() }
func (e *AppError) Unwrap() error { return e.Err }
func (e *AppError) Message() string {
if e.Msg != "" {
return e.Msg
}
switch {
case errors.As(e.Err, new(*os.PathError)),
errors.As(e.Err, new(*os.LinkError)),
errors.As(e.Err, new(*os.SyscallError)),
errors.As(e.Err, new(*net.OpError)):
return "cannot " + e.Error()
default:
return "cannot " + e.Step + ": " + e.Error()
}
}
// Paths contains environment-dependent paths used by hakurei. // Paths contains environment-dependent paths used by hakurei.
type Paths struct { type Paths struct {
// temporary directory returned by [os.TempDir] (usually `/tmp`) // temporary directory returned by [os.TempDir] (usually `/tmp`)

View File

@ -2,93 +2,11 @@ package hst_test
import ( import (
"encoding/json" "encoding/json"
"errors"
"net"
"os"
"syscall"
"testing" "testing"
"hakurei.app/container"
"hakurei.app/container/stub"
"hakurei.app/hst" "hakurei.app/hst"
) )
func TestAppError(t *testing.T) {
testCases := []struct {
name string
err error
s string
message string
is, isF error
}{
{"message", &hst.AppError{Step: "obtain uid from hsu", Err: stub.UniqueError(0),
Msg: "the setuid helper is missing: /run/wrappers/bin/hsu"},
"unique error 0 injected by the test suite",
"the setuid helper is missing: /run/wrappers/bin/hsu",
stub.UniqueError(0), os.ErrNotExist},
{"os.PathError", &hst.AppError{Step: "passthrough os.PathError",
Err: &os.PathError{Op: "stat", Path: "/proc/nonexistent", Err: os.ErrNotExist}},
"stat /proc/nonexistent: file does not exist",
"cannot stat /proc/nonexistent: file does not exist",
os.ErrNotExist, stub.UniqueError(0xdeadbeef)},
{"os.LinkError", &hst.AppError{Step: "passthrough os.LinkError",
Err: &os.LinkError{Op: "link", Old: "/proc/self", New: "/proc/nonexistent", Err: os.ErrNotExist}},
"link /proc/self /proc/nonexistent: file does not exist",
"cannot link /proc/self /proc/nonexistent: file does not exist",
os.ErrNotExist, stub.UniqueError(0xdeadbeef)},
{"os.SyscallError", &hst.AppError{Step: "passthrough os.SyscallError",
Err: &os.SyscallError{Syscall: "meow", Err: syscall.ENOSYS}},
"meow: function not implemented",
"cannot meow: function not implemented",
syscall.ENOSYS, syscall.ENOTRECOVERABLE},
{"net.OpError", &hst.AppError{Step: "passthrough net.OpError",
Err: &net.OpError{Op: "dial", Net: "cat", Err: net.UnknownNetworkError("cat")}},
"dial cat: unknown network cat",
"cannot dial cat: unknown network cat",
net.UnknownNetworkError("cat"), syscall.ENOTRECOVERABLE},
{"default", &hst.AppError{Step: "initialise container configuration", Err: stub.UniqueError(1)},
"unique error 1 injected by the test suite",
"cannot initialise container configuration: unique error 1 injected by the test suite",
stub.UniqueError(1), os.ErrInvalid},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.s {
t.Errorf("Error: %s, want %s", got, tc.s)
}
})
t.Run("message", func(t *testing.T) {
gotMessage, gotMessageOk := container.GetErrorMessage(tc.err)
if want := tc.message != "\x00"; gotMessageOk != want {
t.Errorf("GetErrorMessage: ok = %v, want %v", gotMessage, want)
}
if gotMessageOk {
if gotMessage != tc.message {
t.Errorf("GetErrorMessage: %s, want %s", gotMessage, tc.message)
}
}
})
t.Run("is", func(t *testing.T) {
if !errors.Is(tc.err, tc.is) {
t.Errorf("Is: unexpected false for %v", tc.is)
}
if errors.Is(tc.err, tc.isF) {
t.Errorf("Is: unexpected true for %v", tc.isF)
}
})
})
}
}
func TestTemplate(t *testing.T) { func TestTemplate(t *testing.T) {
const want = `{ const want = `{
"id": "org.chromium.Chromium", "id": "org.chromium.Chromium",

View File

@ -1,28 +1,61 @@
// Package app implements high-level hakurei container behaviour. // Package app defines the generic [App] interface.
package app package app
import ( import (
"context" "context"
"log" "log"
"os" "syscall"
"time"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
) )
// Main runs an app according to [hst.Config] and terminates. Main does not return. type App interface {
func Main(ctx context.Context, config *hst.Config) { // ID returns a copy of [ID] held by App.
var id state.ID ID() state.ID
if err := state.NewAppID(&id); err != nil {
log.Fatal(err)
}
seal := outcome{id: &stringPair[state.ID]{id, id.String()}, syscallDispatcher: direct{}} // Seal determines the outcome of config as a [SealedApp].
if err := seal.finalise(ctx, config); err != nil { // The value of config might be overwritten and must not be used again.
printMessageError("cannot seal app:", err) Seal(config *hst.Config) (SealedApp, error)
os.Exit(1)
}
seal.main() String() string
panic("unreachable") }
type SealedApp interface {
// Run commits sealed system setup and starts the app process.
Run(rs *RunState) error
}
// RunState stores the outcome of a call to [SealedApp.Run].
type RunState struct {
// Time is the exact point in time where the process was created.
// Location must be set to UTC.
//
// Time is nil if no process was ever created.
Time *time.Time
// RevertErr is stored by the deferred revert call.
RevertErr error
// WaitErr is the generic error value created by the standard library.
WaitErr error
syscall.WaitStatus
}
// SetStart stores the current time in [RunState] once.
func (rs *RunState) SetStart() {
if rs.Time != nil {
panic("attempted to store time twice")
}
now := time.Now().UTC()
rs.Time = &now
}
func MustNew(ctx context.Context, os sys.State) App {
a, err := New(ctx, os)
if err != nil {
log.Fatalf("cannot create app: %v", err)
}
return a
} }

69
internal/app/app_linux.go Normal file
View File

@ -0,0 +1,69 @@
package app
import (
"context"
"fmt"
"sync"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
)
func New(ctx context.Context, os sys.State) (App, error) {
a := new(app)
a.sys = os
a.ctx = ctx
id := new(state.ID)
err := state.NewAppID(id)
a.id = newID(id)
return a, err
}
type app struct {
id *stringPair[state.ID]
sys sys.State
ctx context.Context
*outcome
mu sync.RWMutex
}
func (a *app) ID() state.ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
func (a *app) String() string {
if a == nil {
return "(invalid app)"
}
a.mu.RLock()
defer a.mu.RUnlock()
if a.outcome != nil {
if a.outcome.user.uid == nil {
return fmt.Sprintf("(sealed app %s with invalid uid)", a.id)
}
return fmt.Sprintf("(sealed app %s as uid %s)", a.id, a.outcome.user.uid)
}
return fmt.Sprintf("(unsealed app %s)", a.id)
}
func (a *app) Seal(config *hst.Config) (SealedApp, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.outcome != nil {
panic("app sealed twice")
}
seal := new(outcome)
seal.id = a.id
err := seal.finalise(a.ctx, a.sys, config)
if err == nil {
a.outcome = seal
}
return seal, err
}

View File

@ -0,0 +1,106 @@
package app_test
import (
"encoding/json"
"io/fs"
"reflect"
"testing"
"time"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
"hakurei.app/system"
)
type sealTestCase struct {
name string
os sys.State
config *hst.Config
id state.ID
wantSys *system.I
wantContainer *container.Params
}
func TestApp(t *testing.T) {
testCases := append(testCasesPd, testCasesNixos...)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := app.NewWithID(tc.id, tc.os)
var (
gotSys *system.I
gotContainer *container.Params
)
if !t.Run("seal", func(t *testing.T) {
if sa, err := a.Seal(tc.config); err != nil {
hlog.PrintBaseError(err, "got generic error:")
t.Errorf("Seal: error = %v", err)
return
} else {
gotSys, gotContainer = app.AppIParams(a, sa)
}
}) {
return
}
t.Run("compare sys", func(t *testing.T) {
if !gotSys.Equal(tc.wantSys) {
t.Errorf("Seal: sys = %#v, want %#v",
gotSys, tc.wantSys)
}
})
t.Run("compare params", func(t *testing.T) {
if !reflect.DeepEqual(gotContainer, tc.wantContainer) {
t.Errorf("seal: params =\n%s\n, want\n%s",
mustMarshal(gotContainer), mustMarshal(tc.wantContainer))
}
})
})
}
}
func mustMarshal(v any) string {
if b, err := json.Marshal(v); err != nil {
panic(err.Error())
} else {
return string(b)
}
}
func stubDirEntries(names ...string) (e []fs.DirEntry, err error) {
e = make([]fs.DirEntry, len(names))
for i, name := range names {
e[i] = stubDirEntryPath(name)
}
return
}
type stubDirEntryPath string
func (p stubDirEntryPath) Name() string { return string(p) }
func (p stubDirEntryPath) IsDir() bool { panic("attempted to call IsDir") }
func (p stubDirEntryPath) Type() fs.FileMode { panic("attempted to call Type") }
func (p stubDirEntryPath) Info() (fs.FileInfo, error) { panic("attempted to call Info") }
type stubFileInfoMode fs.FileMode
func (s stubFileInfoMode) Name() string { panic("attempted to call Name") }
func (s stubFileInfoMode) Size() int64 { panic("attempted to call Size") }
func (s stubFileInfoMode) Mode() fs.FileMode { return fs.FileMode(s) }
func (s stubFileInfoMode) ModTime() time.Time { panic("attempted to call ModTime") }
func (s stubFileInfoMode) IsDir() bool { panic("attempted to call IsDir") }
func (s stubFileInfoMode) Sys() any { panic("attempted to call Sys") }
type stubFileInfoIsDir bool
func (s stubFileInfoIsDir) Name() string { panic("attempted to call Name") }
func (s stubFileInfoIsDir) Size() int64 { panic("attempted to call Size") }
func (s stubFileInfoIsDir) Mode() fs.FileMode { panic("attempted to call Mode") }
func (s stubFileInfoIsDir) ModTime() time.Time { panic("attempted to call ModTime") }
func (s stubFileInfoIsDir) IsDir() bool { return bool(s) }
func (s stubFileInfoIsDir) Sys() any { panic("attempted to call Sys") }

View File

@ -0,0 +1,167 @@
package app_test
import (
"syscall"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/system"
"hakurei.app/system/acl"
"hakurei.app/system/dbus"
)
func m(pathname string) *container.Absolute { return container.MustAbs(pathname) }
func f(c hst.FilesystemConfig) hst.FilesystemConfigJSON {
return hst.FilesystemConfigJSON{FilesystemConfig: c}
}
var testCasesNixos = []sealTestCase{
{
"nixos chromium direct wayland", new(stubNixOS),
&hst.Config{
ID: "org.chromium.Chromium",
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse),
Shell: m("/run/current-system/sw/bin/zsh"),
Container: &hst.ContainerConfig{
Userns: true, HostNet: true, MapRealUID: true, Env: nil,
Filesystem: []hst.FilesystemConfigJSON{
f(&hst.FSBind{Source: m("/bin")}),
f(&hst.FSBind{Source: m("/usr/bin/")}),
f(&hst.FSBind{Source: m("/nix/store")}),
f(&hst.FSBind{Source: m("/run/current-system")}),
f(&hst.FSBind{Source: m("/sys/block"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/bus"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/class"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/dev"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/devices"), Optional: true}),
f(&hst.FSBind{Source: m("/run/opengl-driver")}),
f(&hst.FSBind{Source: m("/dev/dri"), Device: true, Optional: true}),
f(&hst.FSBind{Source: m("/etc/"), Target: m("/etc/"), Special: true}),
f(&hst.FSBind{Source: m("/var/lib/persist/module/hakurei/0/1"), Write: true, Ensure: true}),
},
},
SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Filter: true,
},
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
},
DirectWayland: true,
Username: "u0_a1",
Home: m("/var/lib/persist/module/hakurei/0/1"),
Identity: 1, Groups: []string{},
},
state.ID{
0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
},
system.New(1000001).
Ensure("/tmp/hakurei.1971", 0711).
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
Ensure("/tmp/hakurei.1971/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/1", acl.Read, acl.Write, acl.Execute).
Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute).
Ensure("/tmp/hakurei.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
Ephemeral(system.Process, "/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
MustProxyDBus("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
}, "/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
&container.Params{
Uid: 1971,
Gid: 100,
Dir: m("/var/lib/persist/module/hakurei/0/1"),
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
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=/run/dbus/system_bus_socket",
"HOME=/var/lib/persist/module/hakurei/0/1",
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
"USER=u0_a1",
"WAYLAND_DISPLAY=wayland-0",
"XDG_RUNTIME_DIR=/run/user/1971",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(container.Ops).
Proc(m("/proc/")).
Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Bind(m("/bin"), m("/bin"), 0).
Bind(m("/usr/bin/"), m("/usr/bin/"), 0).
Bind(m("/nix/store"), m("/nix/store"), 0).
Bind(m("/run/current-system"), m("/run/current-system"), 0).
Bind(m("/sys/block"), m("/sys/block"), container.BindOptional).
Bind(m("/sys/bus"), m("/sys/bus"), container.BindOptional).
Bind(m("/sys/class"), m("/sys/class"), container.BindOptional).
Bind(m("/sys/dev"), m("/sys/dev"), container.BindOptional).
Bind(m("/sys/devices"), m("/sys/devices"), container.BindOptional).
Bind(m("/run/opengl-driver"), m("/run/opengl-driver"), 0).
Bind(m("/dev/dri"), m("/dev/dri"), container.BindDevice|container.BindWritable|container.BindOptional).
Etc(m("/etc/"), "8e2c76b066dabe574cf073bdb46eb5c1").
Bind(m("/var/lib/persist/module/hakurei/0/1"), m("/var/lib/persist/module/hakurei/0/1"), container.BindWritable|container.BindEnsure).
Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.1971/runtime/1"), m("/run/user/1971"), container.BindWritable).
Bind(m("/tmp/hakurei.1971/tmpdir/1"), m("/tmp/"), container.BindWritable).
Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")).
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.Tmp+"/pulse-cookie"), nil).
Bind(m("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
Bind(m("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel,
HostNet: true,
ForwardCancel: true,
},
},
}

View File

@ -0,0 +1,210 @@
package app_test
import (
"os"
"syscall"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/system"
"hakurei.app/system/acl"
"hakurei.app/system/dbus"
)
var testCasesPd = []sealTestCase{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&hst.Config{Username: "chronos", Home: m("/home/chronos")},
state.ID{
0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15,
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
system.New(1000000).
Ensure("/tmp/hakurei.1971", 0711).
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
Ensure("/tmp/hakurei.1971/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/0", acl.Read, acl.Write, acl.Execute).
Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute).
Ensure("/tmp/hakurei.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
&container.Params{
Dir: m("/home/chronos"),
Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"/run/current-system/sw/bin/zsh"},
Env: []string{
"HOME=/home/chronos",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
"USER=chronos",
"XDG_RUNTIME_DIR=/run/user/65534",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(container.Ops).
Root(m("/"), container.BindWritable).
Proc(m("/proc/")).
Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
Readonly(m("/var/run/nscd"), 0755).
Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac").
Tmpfs(m("/run/user/1971"), 8192, 0755).
Tmpfs(m("/run/dbus"), 8192, 0755).
Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.1971/runtime/0"), m("/run/user/65534"), container.BindWritable).
Bind(m("/tmp/hakurei.1971/tmpdir/0"), m("/tmp/"), container.BindWritable).
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true,
HostAbstract: true,
RetainSession: true,
ForwardCancel: true,
},
},
{
"nixos permissive defaults chromium", new(stubNixOS),
&hst.Config{
ID: "org.chromium.Chromium",
Args: []string{"zsh", "-c", "exec chromium "},
Identity: 9,
Groups: []string{"video"},
Username: "chronos",
Home: m("/home/chronos"),
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
},
SystemBus: &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
},
Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse),
},
state.ID{
0xeb, 0xf0, 0x83, 0xd1,
0xb1, 0x75, 0x91, 0x17,
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(1000009).
Ensure("/tmp/hakurei.1971", 0711).
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
Ensure("/tmp/hakurei.1971/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/9", acl.Read, acl.Write, acl.Execute).
Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute).
Ensure("/tmp/hakurei.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Wayland(new(*os.File), "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
}, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
&container.Params{
Dir: m("/home/chronos"),
Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"zsh", "-c", "exec chromium "},
Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
"HOME=/home/chronos",
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
"USER=chronos",
"WAYLAND_DISPLAY=wayland-0",
"XDG_RUNTIME_DIR=/run/user/65534",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(container.Ops).
Root(m("/"), container.BindWritable).
Proc(m("/proc/")).
Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Bind(m("/dev/dri"), m("/dev/dri"), container.BindWritable|container.BindDevice|container.BindOptional).
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
Readonly(m("/var/run/nscd"), 0755).
Etc(m("/etc/"), "ebf083d1b175911782d413369b64ce7c").
Tmpfs(m("/run/user/1971"), 8192, 0755).
Tmpfs(m("/run/dbus"), 8192, 0755).
Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.1971/runtime/9"), m("/run/user/65534"), container.BindWritable).
Bind(m("/tmp/hakurei.1971/tmpdir/9"), m("/tmp/"), container.BindWritable).
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
Bind(m("/tmp/hakurei.1971/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.Tmp+"/pulse-cookie"), nil).
Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0).
Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true,
HostAbstract: true,
RetainSession: true,
ForwardCancel: true,
},
},
}

View File

@ -1,24 +1,34 @@
package app package app_test
import ( import (
"fmt" "fmt"
"io/fs" "io/fs"
"log" "log"
"os/exec"
"os/user" "os/user"
"strconv"
"hakurei.app/hst"
) )
// fs methods are not implemented using a real FS
// to help better understand filesystem access behaviour
type stubNixOS struct { type stubNixOS struct {
lookPathErr map[string]error lookPathErr map[string]error
usernameErr map[string]error usernameErr map[string]error
} }
func (k *stubNixOS) new(func(k syscallDispatcher)) { panic("not implemented") } func (s *stubNixOS) Getuid() int { return 1971 }
func (s *stubNixOS) Getgid() int { return 100 }
func (s *stubNixOS) TempDir() string { return "/tmp" }
func (s *stubNixOS) MustExecutable() string { return "/run/wrappers/bin/hakurei" }
func (s *stubNixOS) Exit(code int) { panic("called exit on stub with code " + strconv.Itoa(code)) }
func (s *stubNixOS) EvalSymlinks(path string) (string, error) { return path, nil }
func (s *stubNixOS) Uid(aid int) (int, error) { return 1000000 + 0*10000 + aid, nil }
func (k *stubNixOS) getuid() int { return 1971 } func (s *stubNixOS) Println(v ...any) { log.Println(v...) }
func (k *stubNixOS) getgid() int { return 100 } func (s *stubNixOS) Printf(format string, v ...any) { log.Printf(format, v...) }
func (k *stubNixOS) lookupEnv(key string) (string, bool) { func (s *stubNixOS) LookupEnv(key string) (string, bool) {
switch key { switch key {
case "SHELL": case "SHELL":
return "/run/current-system/sw/bin/zsh", true return "/run/current-system/sw/bin/zsh", true
@ -30,8 +40,6 @@ func (k *stubNixOS) lookupEnv(key string) (string, bool) {
return "", false return "", false
case "HOME": case "HOME":
return "/home/ophestra", true return "/home/ophestra", true
case "XDG_RUNTIME_DIR":
return "/run/user/1971", true
case "XDG_CONFIG_HOME": case "XDG_CONFIG_HOME":
return "/home/ophestra/xdg/config", true return "/home/ophestra/xdg/config", true
default: default:
@ -39,7 +47,61 @@ func (k *stubNixOS) lookupEnv(key string) (string, bool) {
} }
} }
func (k *stubNixOS) stat(name string) (fs.FileInfo, error) { func (s *stubNixOS) LookPath(file string) (string, error) {
if s.lookPathErr != nil {
if err, ok := s.lookPathErr[file]; ok {
return "", err
}
}
switch file {
case "zsh":
return "/run/current-system/sw/bin/zsh", nil
default:
panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
}
}
func (s *stubNixOS) LookupGroup(name string) (*user.Group, error) {
switch name {
case "video":
return &user.Group{Gid: "26", Name: "video"}, nil
default:
return nil, user.UnknownGroupError(name)
}
}
func (s *stubNixOS) ReadDir(name string) ([]fs.DirEntry, error) {
switch name {
case "/":
return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib",
"lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
case "/run":
return stubDirEntries("agetty.reload", "binfmt", "booted-system",
"credentials", "cryptsetup", "current-system", "dbus", "host", "keys",
"libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager",
"nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm",
"store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2",
"user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state")
case "/etc":
return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default",
"ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid",
"hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd",
"libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm",
"machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc",
"netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf",
"opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1",
"profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba",
"sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh",
"ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc",
"zoneinfo", "zprofile", "zshenv", "zshrc")
default:
panic(fmt.Sprintf("attempted to read unexpected directory %q", name))
}
}
func (s *stubNixOS) Stat(name string) (fs.FileInfo, error) {
switch name { switch name {
case "/var/run/nscd": case "/var/run/nscd":
return nil, nil return nil, nil
@ -56,144 +118,17 @@ func (k *stubNixOS) stat(name string) (fs.FileInfo, error) {
} }
} }
func (k *stubNixOS) readdir(name string) ([]fs.DirEntry, error) { func (s *stubNixOS) Open(name string) (fs.File, error) {
switch name { switch name {
case "/":
return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib",
"lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
case "/run":
return stubDirEntries("agetty.reload", "binfmt", "booted-system",
"credentials", "cryptsetup", "current-system", "dbus", "host", "keys",
"libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager",
"nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm",
"store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2",
"user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state")
case "/etc":
return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default",
"ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid",
"hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd",
"libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm",
"machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc",
"netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf",
"opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1",
"profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba",
"sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh",
"ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc",
"zoneinfo", "zprofile", "zshenv", "zshrc")
default: default:
panic(fmt.Sprintf("attempted to read unexpected directory %q", name)) panic(fmt.Sprintf("attempted to open unexpected file %q", name))
} }
} }
func (k *stubNixOS) tempdir() string { return "/tmp/" } func (s *stubNixOS) Paths() hst.Paths {
return hst.Paths{
func (k *stubNixOS) evalSymlinks(path string) (string, error) { SharePath: m("/tmp/hakurei.1971"),
switch path { RuntimePath: m("/run/user/1971"),
case "/run/user/1971": RunDirPath: m("/run/user/1971/hakurei"),
return "/run/user/1971", nil
case "/tmp/hakurei.0":
return "/tmp/hakurei.0", nil
case "/run/dbus":
return "/run/dbus", nil
case "/dev/kvm":
return "/dev/kvm", nil
case "/etc/":
return "/etc/", nil
case "/bin":
return "/bin", nil
case "/boot":
return "/boot", nil
case "/home":
return "/home", nil
case "/lib":
return "/lib", nil
case "/lib64":
return "/lib64", nil
case "/nix":
return "/nix", nil
case "/root":
return "/root", nil
case "/run":
return "/run", nil
case "/srv":
return "/srv", nil
case "/sys":
return "/sys", nil
case "/usr":
return "/usr", nil
case "/var":
return "/var", nil
case "/dev/dri":
return "/dev/dri", nil
case "/usr/bin/":
return "/usr/bin/", nil
case "/nix/store":
return "/nix/store", nil
case "/run/current-system":
return "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-nixos-system-satori-25.05.99999999.aaaaaaa", nil
case "/sys/block":
return "/sys/block", nil
case "/sys/bus":
return "/sys/bus", nil
case "/sys/class":
return "/sys/class", nil
case "/sys/dev":
return "/sys/dev", nil
case "/sys/devices":
return "/sys/devices", nil
case "/run/opengl-driver":
return "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-graphics-drivers", nil
case "/var/lib/persist/module/hakurei/0/1":
return "/var/lib/persist/module/hakurei/0/1", nil
default:
panic(fmt.Sprintf("attempted to evaluate unexpected path %q", path))
} }
} }
func (k *stubNixOS) lookPath(file string) (string, error) {
if k.lookPathErr != nil {
if err, ok := k.lookPathErr[file]; ok {
return "", err
}
}
switch file {
case "zsh":
return "/run/current-system/sw/bin/zsh", nil
default:
panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
}
}
func (k *stubNixOS) lookupGroupId(name string) (string, error) {
switch name {
case "video":
return "26", nil
default:
return "", user.UnknownGroupError(name)
}
}
func (k *stubNixOS) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
switch cmd.Path {
case "/proc/nonexistent/hsu":
return []byte{'0'}, nil
default:
panic(fmt.Sprintf("unexpected cmd %#v", cmd))
}
}
func (k *stubNixOS) overflowUid() int { return 65534 }
func (k *stubNixOS) overflowGid() int { return 65534 }
func (k *stubNixOS) mustHsuPath() string { return "/proc/nonexistent/hsu" }
func (k *stubNixOS) fatalf(format string, v ...any) { panic(fmt.Sprintf(format, v...)) }
func (k *stubNixOS) isVerbose() bool { return true }
func (k *stubNixOS) verbose(v ...any) { log.Print(v...) }
func (k *stubNixOS) verbosef(format string, v ...any) { log.Printf(format, v...) }

View File

@ -1,452 +0,0 @@
package app
import (
"context"
"encoding/json"
"io/fs"
"os"
"reflect"
"syscall"
"testing"
"time"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/system"
"hakurei.app/system/acl"
"hakurei.app/system/dbus"
)
func TestApp(t *testing.T) {
testCases := []struct {
name string
k syscallDispatcher
config *hst.Config
id state.ID
wantSys *system.I
wantParams *container.Params
}{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&hst.Config{Username: "chronos", Home: m("/home/chronos")},
state.ID{
0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15,
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
system.New(context.TODO(), 1000000).
Ensure("/tmp/hakurei.0", 0711).
Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute).
Ensure("/tmp/hakurei.0/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/0", acl.Read, acl.Write, acl.Execute).
Ensure("/tmp/hakurei.0/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir", acl.Execute).
Ensure("/tmp/hakurei.0/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir/0", acl.Read, acl.Write, acl.Execute),
&container.Params{
Dir: m("/home/chronos"),
Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"/run/current-system/sw/bin/zsh"},
Env: []string{
"HOME=/home/chronos",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
"USER=chronos",
"XDG_RUNTIME_DIR=/run/user/65534",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(container.Ops).
Root(m("/"), container.BindWritable).
Proc(m("/proc/")).
Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777).
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
Readonly(m("/var/run/nscd"), 0755).
Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac").
Tmpfs(m("/run/user/1971"), 8192, 0755).
Tmpfs(m("/run/dbus"), 8192, 0755).
Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.0/runtime/0"), m("/run/user/65534"), container.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), container.BindWritable).
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true,
HostAbstract: true,
RetainSession: true,
ForwardCancel: true,
},
},
{
"nixos permissive defaults chromium", new(stubNixOS),
&hst.Config{
ID: "org.chromium.Chromium",
Args: []string{"zsh", "-c", "exec chromium "},
Identity: 9,
Groups: []string{"video"},
Username: "chronos",
Home: m("/home/chronos"),
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
},
SystemBus: &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
},
Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse),
},
state.ID{
0xeb, 0xf0, 0x83, 0xd1,
0xb1, 0x75, 0x91, 0x17,
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(context.TODO(), 1000009).
Ensure("/tmp/hakurei.0", 0711).
Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute).
Ensure("/tmp/hakurei.0/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/9", acl.Read, acl.Write, acl.Execute).
Ensure("/tmp/hakurei.0/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir", acl.Execute).
Ensure("/tmp/hakurei.0/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c", 0711).
Wayland(new(*os.File), "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
}, "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
&container.Params{
Dir: m("/home/chronos"),
Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"zsh", "-c", "exec chromium "},
Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
"HOME=/home/chronos",
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
"USER=chronos",
"WAYLAND_DISPLAY=wayland-0",
"XDG_RUNTIME_DIR=/run/user/65534",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(container.Ops).
Root(m("/"), container.BindWritable).
Proc(m("/proc/")).
Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777).
Bind(m("/dev/dri"), m("/dev/dri"), container.BindWritable|container.BindDevice|container.BindOptional).
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
Readonly(m("/var/run/nscd"), 0755).
Etc(m("/etc/"), "ebf083d1b175911782d413369b64ce7c").
Tmpfs(m("/run/user/1971"), 8192, 0755).
Tmpfs(m("/run/dbus"), 8192, 0755).
Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/65534"), container.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/9"), m("/tmp/"), container.BindWritable).
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
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.Tmp+"/pulse-cookie"), nil).
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0).
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true,
HostAbstract: true,
RetainSession: true,
ForwardCancel: true,
},
},
{
"nixos chromium direct wayland", new(stubNixOS),
&hst.Config{
ID: "org.chromium.Chromium",
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse),
Shell: m("/run/current-system/sw/bin/zsh"),
Container: &hst.ContainerConfig{
Userns: true, HostNet: true, MapRealUID: true, Env: nil,
Filesystem: []hst.FilesystemConfigJSON{
f(&hst.FSBind{Source: m("/bin")}),
f(&hst.FSBind{Source: m("/usr/bin/")}),
f(&hst.FSBind{Source: m("/nix/store")}),
f(&hst.FSBind{Source: m("/run/current-system")}),
f(&hst.FSBind{Source: m("/sys/block"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/bus"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/class"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/dev"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/devices"), Optional: true}),
f(&hst.FSBind{Source: m("/run/opengl-driver")}),
f(&hst.FSBind{Source: m("/dev/dri"), Device: true, Optional: true}),
f(&hst.FSBind{Source: m("/etc/"), Target: m("/etc/"), Special: true}),
f(&hst.FSBind{Source: m("/var/lib/persist/module/hakurei/0/1"), Write: true, Ensure: true}),
},
},
SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Filter: true,
},
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
},
DirectWayland: true,
Username: "u0_a1",
Home: m("/var/lib/persist/module/hakurei/0/1"),
Identity: 1, Groups: []string{},
},
state.ID{
0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
},
system.New(context.TODO(), 1000001).
Ensure("/tmp/hakurei.0", 0711).
Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute).
Ensure("/tmp/hakurei.0/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/1", acl.Read, acl.Write, acl.Execute).
Ensure("/tmp/hakurei.0/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir", acl.Execute).
Ensure("/tmp/hakurei.0/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir/1", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
Ephemeral(system.Process, "/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
MustProxyDBus("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
}, "/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
&container.Params{
Uid: 1971,
Gid: 100,
Dir: m("/var/lib/persist/module/hakurei/0/1"),
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
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=/run/dbus/system_bus_socket",
"HOME=/var/lib/persist/module/hakurei/0/1",
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
"USER=u0_a1",
"WAYLAND_DISPLAY=wayland-0",
"XDG_RUNTIME_DIR=/run/user/1971",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(container.Ops).
Proc(m("/proc/")).
Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777).
Bind(m("/bin"), m("/bin"), 0).
Bind(m("/usr/bin/"), m("/usr/bin/"), 0).
Bind(m("/nix/store"), m("/nix/store"), 0).
Bind(m("/run/current-system"), m("/run/current-system"), 0).
Bind(m("/sys/block"), m("/sys/block"), container.BindOptional).
Bind(m("/sys/bus"), m("/sys/bus"), container.BindOptional).
Bind(m("/sys/class"), m("/sys/class"), container.BindOptional).
Bind(m("/sys/dev"), m("/sys/dev"), container.BindOptional).
Bind(m("/sys/devices"), m("/sys/devices"), container.BindOptional).
Bind(m("/run/opengl-driver"), m("/run/opengl-driver"), 0).
Bind(m("/dev/dri"), m("/dev/dri"), container.BindDevice|container.BindWritable|container.BindOptional).
Etc(m("/etc/"), "8e2c76b066dabe574cf073bdb46eb5c1").
Bind(m("/var/lib/persist/module/hakurei/0/1"), m("/var/lib/persist/module/hakurei/0/1"), container.BindWritable|container.BindEnsure).
Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), container.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), container.BindWritable).
Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")).
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.Tmp+"/pulse-cookie"), nil).
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel,
HostNet: true,
ForwardCancel: true,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("finalise", func(t *testing.T) {
seal := outcome{syscallDispatcher: tc.k, id: &stringPair[state.ID]{tc.id, tc.id.String()}}
err := seal.finalise(t.Context(), tc.config)
if err != nil {
if s, ok := container.GetErrorMessage(err); !ok {
t.Fatalf("Seal: error = %v", err)
} else {
t.Fatalf("Seal: %s", s)
}
}
t.Run("sys", func(t *testing.T) {
if !seal.sys.Equal(tc.wantSys) {
t.Errorf("Seal: sys = %#v, want %#v", seal.sys, tc.wantSys)
}
})
t.Run("params", func(t *testing.T) {
if !reflect.DeepEqual(seal.container, tc.wantParams) {
t.Errorf("seal: container =\n%s\n, want\n%s", mustMarshal(seal.container), mustMarshal(tc.wantParams))
}
})
})
})
}
}
func mustMarshal(v any) string {
if b, err := json.Marshal(v); err != nil {
panic(err.Error())
} else {
return string(b)
}
}
func stubDirEntries(names ...string) (e []fs.DirEntry, err error) {
e = make([]fs.DirEntry, len(names))
for i, name := range names {
e[i] = stubDirEntryPath(name)
}
return
}
type stubDirEntryPath string
func (p stubDirEntryPath) Name() string { return string(p) }
func (p stubDirEntryPath) IsDir() bool { panic("attempted to call IsDir") }
func (p stubDirEntryPath) Type() fs.FileMode { panic("attempted to call Type") }
func (p stubDirEntryPath) Info() (fs.FileInfo, error) { panic("attempted to call Info") }
type stubFileInfoMode fs.FileMode
func (s stubFileInfoMode) Name() string { panic("attempted to call Name") }
func (s stubFileInfoMode) Size() int64 { panic("attempted to call Size") }
func (s stubFileInfoMode) Mode() fs.FileMode { return fs.FileMode(s) }
func (s stubFileInfoMode) ModTime() time.Time { panic("attempted to call ModTime") }
func (s stubFileInfoMode) IsDir() bool { panic("attempted to call IsDir") }
func (s stubFileInfoMode) Sys() any { panic("attempted to call Sys") }
type stubFileInfoIsDir bool
func (s stubFileInfoIsDir) Name() string { panic("attempted to call Name") }
func (s stubFileInfoIsDir) Size() int64 { panic("attempted to call Size") }
func (s stubFileInfoIsDir) Mode() fs.FileMode { panic("attempted to call Mode") }
func (s stubFileInfoIsDir) ModTime() time.Time { panic("attempted to call ModTime") }
func (s stubFileInfoIsDir) IsDir() bool { return bool(s) }
func (s stubFileInfoIsDir) Sys() any { panic("attempted to call Sys") }
func m(pathname string) *container.Absolute {
return container.MustAbs(pathname)
}
func f(c hst.FilesystemConfig) hst.FilesystemConfigJSON {
return hst.FilesystemConfigJSON{FilesystemConfig: c}
}

View File

@ -11,23 +11,20 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
// in practice there should be less than 30 system mount points // in practice there should be less than 30 entries added by the runtime;
// allocating slightly more as a margin for future expansion
const preallocateOpsCount = 1 << 5 const preallocateOpsCount = 1 << 5
// newContainer initialises [container.Params] via [hst.ContainerConfig]. // newContainer initialises [container.Params] via [hst.ContainerConfig].
// Note that remaining container setup must be queued by the caller. // Note that remaining container setup must be queued by the caller.
func newContainer( func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid *int) (*container.Params, map[string]string, error) {
k syscallDispatcher,
s *hst.ContainerConfig,
prefix string,
sc *hst.Paths,
uid, gid *int,
) (*container.Params, map[string]string, error) {
if s == nil { if s == nil {
return nil, nil, newWithMessage("invalid container configuration") return nil, nil, hlog.WrapErr(syscall.EBADE, "invalid container configuration")
} }
params := &container.Params{ params := &container.Params{
@ -43,7 +40,9 @@ func newContainer(
ForwardCancel: s.WaitDelay >= 0, ForwardCancel: s.WaitDelay >= 0,
} }
as := &hst.ApplyState{AutoEtcPrefix: prefix} as := &hst.ApplyState{
AutoEtcPrefix: prefix,
}
{ {
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)) ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem))
params.Ops = &ops params.Ops = &ops
@ -68,13 +67,15 @@ func newContainer(
} }
if s.MapRealUID { if s.MapRealUID {
params.Uid = k.getuid() /* some programs fail to connect to dbus session running as a different uid
so this workaround is introduced to map priv-side caller uid in container */
params.Uid = os.Getuid()
*uid = params.Uid *uid = params.Uid
params.Gid = k.getgid() params.Gid = os.Getgid()
*gid = params.Gid *gid = params.Gid
} else { } else {
*uid = k.overflowUid() *uid = container.OverflowUid()
*gid = k.overflowGid() *gid = container.OverflowGid()
} }
filesystem := s.Filesystem filesystem := s.Filesystem
@ -101,15 +102,13 @@ func newContainer(
} else { } else {
params.Bind(container.AbsFHSDev, container.AbsFHSDev, container.BindWritable|container.BindDevice) params.Bind(container.AbsFHSDev, container.AbsFHSDev, container.BindWritable|container.BindDevice)
} }
// /dev is mounted readonly later on, this prevents /dev/shm from going readonly with it
params.Tmpfs(container.AbsFHSDev.Append("shm"), 0, 01777)
/* retrieve paths and hide them if they're made available in the sandbox; /* retrieve paths and hide them if they're made available in the sandbox;
this feature tries to improve user experience of permissive defaults, and this feature tries to improve user experience of permissive defaults, and
to warn about issues in custom configuration; it is NOT a security feature to warn about issues in custom configuration; it is NOT a security feature
and should not be treated as such, ALWAYS be careful with what you bind */ and should not be treated as such, ALWAYS be careful with what you bind */
var hidePaths []string var hidePaths []string
sc := os.Paths()
hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String()) hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String())
_, systemBusAddr := dbus.Address() _, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil { if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
@ -126,11 +125,11 @@ func newContainer(
// get parent dir of socket // get parent dir of socket
dir := path.Dir(pair[1]) dir := path.Dir(pair[1])
if dir == "." || dir == container.FHSRoot { if dir == "." || dir == container.FHSRoot {
k.verbosef("dbus socket %q is in an unusual location", pair[1]) os.Printf("dbus socket %q is in an unusual location", pair[1])
} }
hidePaths = append(hidePaths, dir) hidePaths = append(hidePaths, dir)
} else { } else {
k.verbosef("dbus socket %q is not absolute", pair[1]) os.Printf("dbus socket %q is not absolute", pair[1])
} }
} }
} }
@ -138,7 +137,7 @@ func newContainer(
} }
hidePathMatch := make([]bool, len(hidePaths)) hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths { for i := range hidePaths {
if err := evalSymlinks(k, &hidePaths[i]); err != nil { if err := evalSymlinks(os, &hidePaths[i]); err != nil {
return nil, nil, err return nil, nil, err
} }
} }
@ -157,7 +156,7 @@ func newContainer(
// AutoRootOp is a collection of many BindMountOp internally // AutoRootOp is a collection of many BindMountOp internally
var autoRootEntries []fs.DirEntry var autoRootEntries []fs.DirEntry
if autoroot != nil { if autoroot != nil {
if d, err := k.readdir(autoroot.Source.String()); err != nil { if d, err := os.ReadDir(autoroot.Source.String()); err != nil {
return nil, nil, err return nil, nil, err
} else { } else {
// autoroot counter // autoroot counter
@ -193,7 +192,7 @@ func newContainer(
} }
hidePathSourceEval[i] = [2]string{a.String(), a.String()} hidePathSourceEval[i] = [2]string{a.String(), a.String()}
if err := evalSymlinks(k, &hidePathSourceEval[i][0]); err != nil { if err := evalSymlinks(os, &hidePathSourceEval[i][0]); err != nil {
return nil, nil, err return nil, nil, err
} }
} }
@ -209,7 +208,7 @@ func newContainer(
return nil, nil, err return nil, nil, err
} else if ok { } else if ok {
hidePathMatch[i] = true hidePathMatch[i] = true
k.verbosef("hiding path %q from %q", hidePaths[i], p[1]) os.Printf("hiding path %q from %q", hidePaths[i], p[1])
} }
} }
} }
@ -240,13 +239,12 @@ func newContainer(
return params, maps.Clone(s.Env), nil return params, maps.Clone(s.Env), nil
} }
// evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist]. func evalSymlinks(os sys.State, v *string) error {
func evalSymlinks(k syscallDispatcher, v *string) error { if p, err := os.EvalSymlinks(*v); err != nil {
if p, err := k.evalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return err return err
} }
k.verbosef("path %q does not yet exist", *v) os.Printf("path %q does not yet exist", *v)
} else { } else {
*v = p *v = p
} }

View File

@ -1,99 +0,0 @@
package app
import (
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
type syscallDispatcher interface {
// new starts a goroutine with a new instance of syscallDispatcher.
// A syscallDispatcher must never be used in any goroutine other than the one owning it,
// just synchronising access is not enough, as this is for test instrumentation.
new(f func(k syscallDispatcher))
// getuid provides [os.Getuid].
getuid() int
// getgid provides [os.Getgid].
getgid() int
// lookupEnv provides [os.LookupEnv].
lookupEnv(key string) (string, bool)
// stat provides [os.Stat].
stat(name string) (os.FileInfo, error)
// readdir provides [os.ReadDir].
readdir(name string) ([]os.DirEntry, error)
// tempdir provides [os.TempDir].
tempdir() string
// evalSymlinks provides [filepath.EvalSymlinks].
evalSymlinks(path string) (string, error)
// lookPath provides exec.LookPath.
lookPath(file string) (string, error)
// lookupGroupId calls [user.LookupGroup] and returns the Gid field of the resulting [user.Group] struct.
lookupGroupId(name string) (string, error)
// cmdOutput provides the Output method of [exec.Cmd].
cmdOutput(cmd *exec.Cmd) ([]byte, error)
// overflowUid provides [container.OverflowUid].
overflowUid() int
// overflowGid provides [container.OverflowGid].
overflowGid() int
// mustHsuPath provides [internal.MustHsuPath].
mustHsuPath() string
// fatalf provides [log.Fatalf].
fatalf(format string, v ...any)
isVerbose() bool
verbose(v ...any)
verbosef(format string, v ...any)
}
// direct implements syscallDispatcher on the current kernel.
type direct struct{}
func (k direct) new(f func(k syscallDispatcher)) { go f(k) }
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) }
func (direct) stat(name string) (os.FileInfo, error) { return os.Stat(name) }
func (direct) readdir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
func (direct) tempdir() string { return os.TempDir() }
func (direct) evalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) }
func (direct) lookPath(file string) (string, error) { return exec.LookPath(file) }
func (direct) lookupGroupId(name string) (gid string, err error) {
var group *user.Group
group, err = user.LookupGroup(name)
if group != nil {
gid = group.Gid
}
return
}
func (direct) cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() }
func (direct) overflowUid() int { return container.OverflowUid() }
func (direct) overflowGid() int { return container.OverflowGid() }
func (direct) mustHsuPath() string { return internal.MustHsuPath() }
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }
func (k direct) isVerbose() bool { return hlog.Load() }
func (direct) verbose(v ...any) { hlog.Verbose(v...) }
func (direct) verbosef(format string, v ...any) { hlog.Verbosef(format, v...) }

181
internal/app/errors.go Normal file
View File

@ -0,0 +1,181 @@
package app
import (
"errors"
"log"
"hakurei.app/internal/hlog"
)
func PrintRunStateErr(rs *RunState, runErr error) (code int) {
code = rs.ExitStatus()
if runErr != nil {
if rs.Time == nil {
hlog.PrintBaseError(runErr, "cannot start app:")
} else {
var e *hlog.BaseError
if !hlog.AsBaseError(runErr, &e) {
log.Println("wait failed:", runErr)
} else {
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
var se *StateStoreError
if !errors.As(runErr, &se) {
// does not need special handling
log.Print(e.Message())
} else {
// inner error are either unwrapped store errors
// or joined errors returned by *appSealTx revert
// wrapped in *app.BaseError
var ej RevertCompoundError
if !errors.As(se.InnerErr, &ej) {
// does not require special handling
log.Print(e.Message())
} else {
errs := ej.Unwrap()
// every error here is wrapped in *app.BaseError
for _, ei := range errs {
var eb *hlog.BaseError
if !errors.As(ei, &eb) {
// unreachable
log.Println("invalid error type returned by revert:", ei)
} else {
// print inner *app.BaseError message
log.Print(eb.Message())
}
}
}
}
}
}
if code == 0 {
code = 126
}
}
if rs.RevertErr != nil {
var stateStoreError *StateStoreError
if !errors.As(rs.RevertErr, &stateStoreError) || stateStoreError == nil {
hlog.PrintBaseError(rs.RevertErr, "generic fault during cleanup:")
goto out
}
if stateStoreError.Err != nil {
if len(stateStoreError.Err) == 2 {
if stateStoreError.Err[0] != nil {
if joinedErrs, ok := stateStoreError.Err[0].(interface{ Unwrap() []error }); !ok {
hlog.PrintBaseError(stateStoreError.Err[0], "generic fault during revert:")
} else {
for _, err := range joinedErrs.Unwrap() {
if err != nil {
hlog.PrintBaseError(err, "fault during revert:")
}
}
}
}
if stateStoreError.Err[1] != nil {
log.Printf("cannot close store: %v", stateStoreError.Err[1])
}
} else {
log.Printf("fault during cleanup: %v",
errors.Join(stateStoreError.Err...))
}
}
if stateStoreError.OpErr != nil {
log.Printf("blind revert due to store fault: %v",
stateStoreError.OpErr)
}
if stateStoreError.DoErr != nil {
hlog.PrintBaseError(stateStoreError.DoErr, "state store operation unsuccessful:")
}
if stateStoreError.Inner && stateStoreError.InnerErr != nil {
hlog.PrintBaseError(stateStoreError.InnerErr, "cannot destroy state entry:")
}
out:
if code == 0 {
code = 128
}
}
if rs.WaitErr != nil {
hlog.Verbosef("wait: %v", rs.WaitErr)
}
return
}
// StateStoreError is returned for a failed state save
type StateStoreError struct {
// whether inner function was called
Inner bool
// returned by the Save/Destroy method of [state.Cursor]
InnerErr error
// returned by the Do method of [state.Store]
DoErr error
// stores an arbitrary store operation error
OpErr error
// stores arbitrary errors
Err []error
}
// save saves arbitrary errors in [StateStoreError] once.
func (e *StateStoreError) save(errs ...error) {
if len(errs) == 0 || e.Err != nil {
panic("invalid call to save")
}
e.Err = errs
}
func (e *StateStoreError) equiv(a ...any) error {
if e.Inner && e.InnerErr == nil && e.DoErr == nil && e.OpErr == nil && errors.Join(e.Err...) == nil {
return nil
} else {
return hlog.WrapErrSuffix(e, a...)
}
}
func (e *StateStoreError) Error() string {
if e.Inner && e.InnerErr != nil {
return e.InnerErr.Error()
}
if e.DoErr != nil {
return e.DoErr.Error()
}
if e.OpErr != nil {
return e.OpErr.Error()
}
if err := errors.Join(e.Err...); err != nil {
return err.Error()
}
// equiv nullifies e for values where this is reached
panic("unreachable")
}
func (e *StateStoreError) Unwrap() (errs []error) {
errs = make([]error, 0, 3)
if e.InnerErr != nil {
errs = append(errs, e.InnerErr)
}
if e.DoErr != nil {
errs = append(errs, e.DoErr)
}
if e.OpErr != nil {
errs = append(errs, e.OpErr)
}
if err := errors.Join(e.Err...); err != nil {
errs = append(errs, err)
}
return
}
// A RevertCompoundError encapsulates errors returned by
// the Revert method of [system.I].
type RevertCompoundError interface {
Error() string
Unwrap() []error
}

View File

@ -0,0 +1,24 @@
package app
import (
"hakurei.app/container"
"hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
"hakurei.app/system"
)
func NewWithID(id state.ID, os sys.State) App {
a := new(app)
a.id = newID(&id)
a.sys = os
return a
}
func AppIParams(a App, sa SealedApp) (*system.I, *container.Params) {
v := a.(*app)
seal := sa.(*outcome)
if v.outcome != seal || v.id != seal.id {
panic("broken app/outcome link")
}
return seal.sys, seal.container
}

View File

@ -1,96 +0,0 @@
package app
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"sync"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
// Hsu caches responses from cmd/hsu.
type Hsu struct {
idOnce sync.Once
idErr error
id int
kOnce sync.Once
k syscallDispatcher
}
var ErrHsuAccess = errors.New("current user is not in the hsurc file")
// ensureDispatcher ensures Hsu.k is not nil.
func (h *Hsu) ensureDispatcher() {
h.kOnce.Do(func() {
if h.k == nil {
h.k = direct{}
}
})
}
// ID returns the current user hsurc identifier. ErrHsuAccess is returned if the current user is not in hsurc.
func (h *Hsu) ID() (int, error) {
h.ensureDispatcher()
h.idOnce.Do(func() {
h.id = -1
hsuPath := h.k.mustHsuPath()
cmd := exec.Command(hsuPath)
cmd.Path = hsuPath
cmd.Stderr = os.Stderr // pass through fatal messages
cmd.Env = make([]string, 0)
cmd.Dir = container.FHSRoot
var (
p []byte
exitError *exec.ExitError
)
const step = "obtain uid from hsu"
if p, h.idErr = h.k.cmdOutput(cmd); h.idErr == nil {
h.id, h.idErr = strconv.Atoi(string(p))
if h.idErr != nil {
h.idErr = &hst.AppError{Step: step, Err: h.idErr, Msg: "invalid uid string from hsu"}
}
} else if errors.As(h.idErr, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
// hsu prints an error message in this case
h.idErr = &hst.AppError{Step: step, Err: ErrHsuAccess}
} else if os.IsNotExist(h.idErr) {
h.idErr = &hst.AppError{Step: step, Err: os.ErrNotExist,
Msg: fmt.Sprintf("the setuid helper is missing: %s", hsuPath)}
}
})
return h.id, h.idErr
}
// MustID calls [Hsu.ID] and terminates on error.
func (h *Hsu) MustID() int {
id, err := h.ID()
if err == nil {
return id
}
const fallback = "cannot retrieve user id from setuid wrapper:"
if errors.Is(err, ErrHsuAccess) {
hlog.Verbose("*"+fallback, err)
os.Exit(1)
return -0xdeadbeef
} else if m, ok := container.GetErrorMessage(err); ok {
log.Fatal(m)
return -0xdeadbeef
} else {
log.Fatalln(fallback, err)
return -0xdeadbeef
}
}
// HsuUid returns target uid for the stable hsu uid format.
// No bounds check is performed, a value retrieved from hsu is expected.
func HsuUid(id, identity int) int { return 1000000 + id*10000 + identity }

View File

@ -1,36 +0,0 @@
package app
import (
"strconv"
"hakurei.app/container"
"hakurei.app/hst"
)
// CopyPaths populates a [hst.Paths] struct.
func CopyPaths(v *hst.Paths, userid int) { copyPaths(direct{}, v, userid) }
// copyPaths populates a [hst.Paths] struct.
func copyPaths(k syscallDispatcher, v *hst.Paths, userid int) {
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
if tempDir, err := container.NewAbs(k.tempdir()); err != nil {
k.fatalf("invalid TMPDIR: %v", err)
} else {
v.TempDir = tempDir
}
v.SharePath = v.TempDir.Append("hakurei." + strconv.Itoa(userid))
k.verbosef("process share directory at %q", v.SharePath)
r, _ := k.lookupEnv(xdgRuntimeDir)
if a, err := container.NewAbs(r); err != nil {
// fall back to path in share since hakurei has no hard XDG dependency
v.RunDirPath = v.SharePath.Append("run")
v.RuntimePath = v.RunDirPath.Append("compat")
} else {
v.RuntimePath = a
v.RunDirPath = v.RuntimePath.Append("hakurei")
}
k.verbosef("runtime directory at %q", v.RunDirPath)
}

View File

@ -1,339 +0,0 @@
package app
import (
"context"
"encoding/gob"
"errors"
"log"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system"
)
// duration to wait for shim to exit, after container WaitDelay has elapsed.
const shimWaitTimeout = 5 * time.Second
// mainState holds persistent state bound to outcome.main.
type mainState struct {
// done is whether beforeExit has been called already.
done bool
// Time is the exact point in time where the process was created.
// Location must be set to UTC.
//
// Time is nil if no process was ever created.
Time *time.Time
store state.Store
cancel context.CancelFunc
cmd *exec.Cmd
cmdWait chan error
k *outcome
uintptr
}
const (
// mainNeedsRevert indicates the call to Commit has succeeded.
mainNeedsRevert uintptr = 1 << iota
// mainNeedsDestroy indicates the instance state entry is present in the store.
mainNeedsDestroy
)
// beforeExit must be called immediately before a call to [os.Exit].
func (ms mainState) beforeExit(isFault bool) {
if ms.done {
panic("attempting to call beforeExit twice")
}
ms.done = true
defer hlog.BeforeExit()
if isFault && ms.cancel != nil {
ms.cancel()
}
var hasErr bool
// updates hasErr but does not terminate
perror := func(err error, message string) {
hasErr = true
printMessageError("cannot "+message+":", err)
}
exitCode := 1
defer func() {
if hasErr {
os.Exit(exitCode)
}
}()
// this also handles wait for a non-fault termination
if ms.cmd != nil && ms.cmdWait != nil {
waitDone := make(chan struct{})
// TODO(ophestra): enforce this limit early so it does not have to be done twice
shimTimeoutCompensated := shimWaitTimeout
if ms.k.waitDelay > MaxShimWaitDelay {
shimTimeoutCompensated += MaxShimWaitDelay
} else {
shimTimeoutCompensated += ms.k.waitDelay
}
// this ties waitDone to ctx with the additional compensated timeout duration
go func() { <-ms.k.ctx.Done(); time.Sleep(shimTimeoutCompensated); close(waitDone) }()
select {
case err := <-ms.cmdWait:
wstatus, ok := ms.cmd.ProcessState.Sys().(syscall.WaitStatus)
if ok {
if v := wstatus.ExitStatus(); v != 0 {
hasErr = true
exitCode = v
}
}
if hlog.Load() {
if !ok {
if err != nil {
hlog.Verbosef("wait: %v", err)
}
} else {
switch {
case wstatus.Exited():
hlog.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus())
case wstatus.CoreDump():
hlog.Verbosef("process %d dumped core", ms.cmd.Process.Pid)
case wstatus.Signaled():
hlog.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal())
default:
hlog.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus)
}
}
}
case <-waitDone:
hlog.Resume()
// this is only reachable when shim did not exit within shimWaitTimeout, after its WaitDelay has elapsed.
// This is different from the container failing to terminate within its timeout period, as that is enforced
// by the shim. This path is instead reached when there is a lockup in shim preventing it from completing.
log.Printf("process %d did not terminate", ms.cmd.Process.Pid)
}
hlog.Resume()
if ms.k.sync != nil {
if err := ms.k.sync.Close(); err != nil {
perror(err, "close wayland security context")
}
}
if ms.k.dbusMsg != nil {
ms.k.dbusMsg()
}
}
if ms.uintptr&mainNeedsRevert != 0 {
if ok, err := ms.store.Do(ms.k.user.identity.unwrap(), func(c state.Cursor) {
if ms.uintptr&mainNeedsDestroy != 0 {
if err := c.Destroy(ms.k.id.unwrap()); err != nil {
perror(err, "destroy state entry")
}
}
var rt system.Enablement
if states, err := c.Load(); err != nil {
// it is impossible to continue from this point;
// revert per-process state here to limit damage
ec := system.Process
if revertErr := ms.k.sys.Revert((*system.Criteria)(&ec)); revertErr != nil {
var joinError interface {
Unwrap() []error
error
}
if !errors.As(revertErr, &joinError) || joinError == nil {
perror(revertErr, "revert system setup")
} else {
for _, v := range joinError.Unwrap() {
perror(v, "revert system setup step")
}
}
}
perror(err, "load instance states")
} else {
ec := system.Process
if l := len(states); l == 0 {
ec |= system.User
} else {
hlog.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
}
// accumulate enablements of remaining launchers
for i, s := range states {
if s.Config != nil {
rt |= s.Config.Enablements.Unwrap()
} else {
log.Printf("state entry %d does not contain config", i)
}
}
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
if hlog.Load() {
if ec > 0 {
hlog.Verbose("reverting operations scope", system.TypeString(ec))
}
}
if err = ms.k.sys.Revert((*system.Criteria)(&ec)); err != nil {
perror(err, "revert system setup")
}
}
}); err != nil {
if ok {
perror(err, "unlock state store")
} else {
perror(err, "open state store")
}
}
} else if ms.uintptr&mainNeedsDestroy != 0 {
panic("unreachable")
}
if ms.store != nil {
if err := ms.store.Close(); err != nil {
perror(err, "close state store")
}
}
}
// fatal calls printMessageError, performs necessary cleanup, followed by a call to [os.Exit](1).
func (ms mainState) fatal(fallback string, ferr error) {
printMessageError(fallback, ferr)
ms.beforeExit(true)
os.Exit(1)
}
// main carries out outcome and terminates. main does not return.
func (k *outcome) main() {
if !k.active.CompareAndSwap(false, true) {
panic("outcome: attempted to run twice")
}
// read comp value early for early failure
hsuPath := internal.MustHsuPath()
// ms.beforeExit required beyond this point
ms := &mainState{k: k}
if err := k.sys.Commit(); err != nil {
ms.fatal("cannot commit system setup:", err)
}
ms.uintptr |= mainNeedsRevert
ms.store = state.NewMulti(k.runDirPath.String())
ctx, cancel := context.WithCancel(k.ctx)
defer cancel()
ms.cancel = cancel
ms.cmd = exec.CommandContext(ctx, hsuPath)
ms.cmd.Stdin, ms.cmd.Stdout, ms.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
ms.cmd.Dir = container.FHSRoot // container init enters final working directory
// shim runs in the same session as monitor; see shim.go for behaviour
ms.cmd.Cancel = func() error { return ms.cmd.Process.Signal(syscall.SIGCONT) }
var e *gob.Encoder
if fd, encoder, err := container.Setup(&ms.cmd.ExtraFiles); err != nil {
ms.fatal("cannot create shim setup pipe:", err)
} else {
e = encoder
ms.cmd.Env = []string{
// passed through to shim by hsu
shimEnv + "=" + strconv.Itoa(fd),
// interpreted by hsu
"HAKUREI_IDENTITY=" + k.user.identity.String(),
}
}
if len(k.user.supp) > 0 {
hlog.Verbosef("attaching supplementary group ids %s", k.user.supp)
// interpreted by hsu
ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(k.user.supp, " "))
}
hlog.Verbosef("setuid helper at %s", hsuPath)
hlog.Suspend()
if err := ms.cmd.Start(); err != nil {
ms.fatal("cannot start setuid wrapper:", err)
}
startTime := time.Now().UTC()
ms.cmdWait = make(chan error, 1)
// this ties context back to the life of the process
go func() { ms.cmdWait <- ms.cmd.Wait(); cancel() }()
ms.Time = &startTime
// unfortunately the I/O here cannot be directly canceled;
// the cancellation path leads to fatal in this case so that is fine
select {
case err := <-func() (setupErr chan error) {
setupErr = make(chan error, 1)
go func() {
setupErr <- e.Encode(&shimParams{
os.Getpid(),
k.waitDelay,
k.container,
hlog.Load(),
})
}()
return
}():
if err != nil {
hlog.Resume()
ms.fatal("cannot transmit shim config:", err)
}
case <-ctx.Done():
hlog.Resume()
ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err()))
}
// shim accepted setup payload, create process state
if ok, err := ms.store.Do(k.user.identity.unwrap(), func(c state.Cursor) {
if err := c.Save(&state.State{
ID: k.id.unwrap(),
PID: ms.cmd.Process.Pid,
Time: *ms.Time,
}, k.ct); err != nil {
ms.fatal("cannot save state entry:", err)
}
}); err != nil {
if ok {
ms.uintptr |= mainNeedsDestroy
ms.fatal("cannot unlock state store:", err)
} else {
ms.fatal("cannot open state store:", err)
}
}
// state in store at this point, destroy defunct state entry on termination
ms.uintptr |= mainNeedsDestroy
// beforeExit ties shim process to context
ms.beforeExit(false)
os.Exit(0)
}
// 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 := container.GetErrorMessage(err)
if !ok {
log.Println(fallback, err)
return
}
log.Print(m)
}

View File

@ -0,0 +1,201 @@
package app
import (
"context"
"encoding/gob"
"errors"
"log"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system"
)
const shimWaitTimeout = 5 * time.Second
func (seal *outcome) Run(rs *RunState) error {
if !seal.f.CompareAndSwap(false, true) {
// run does much more than just starting a process; calling it twice, even if the first call fails, will result
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
// other Run a chance to return
return errors.New("outcome: attempted to run twice")
}
if rs == nil {
panic("invalid state")
}
// read comp value early to allow for early failure
hsuPath := internal.MustHsuPath()
if err := seal.sys.Commit(seal.ctx); err != nil {
return err
}
store := state.NewMulti(seal.runDirPath.String())
deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store
defer func() {
var revertErr error
storeErr := new(StateStoreError)
storeErr.Inner, storeErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
revertErr = func() error {
storeErr.InnerErr = deferredStoreFunc(c)
var rt system.Enablement
ec := system.Process
if states, err := c.Load(); err != nil {
// revert per-process state here to limit damage
storeErr.OpErr = err
return seal.sys.Revert((*system.Criteria)(&ec))
} else {
if l := len(states); l == 0 {
ec |= system.User
} else {
hlog.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
}
// accumulate enablements of remaining launchers
for i, s := range states {
if s.Config != nil {
rt |= s.Config.Enablements.Unwrap()
} else {
log.Printf("state entry %d does not contain config", i)
}
}
}
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
if hlog.Load() {
if ec > 0 {
hlog.Verbose("reverting operations scope", system.TypeString(ec))
}
}
return seal.sys.Revert((*system.Criteria)(&ec))
}()
})
storeErr.save(revertErr, store.Close())
rs.RevertErr = storeErr.equiv("error during cleanup:")
}()
ctx, cancel := context.WithCancel(seal.ctx)
defer cancel()
cmd := exec.CommandContext(ctx, hsuPath)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Dir = container.FHSRoot // container init enters final working directory
// shim runs in the same session as monitor; see shim.go for behaviour
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
var e *gob.Encoder
if fd, encoder, err := container.Setup(&cmd.ExtraFiles); err != nil {
return hlog.WrapErrSuffix(err,
"cannot create shim setup pipe:")
} else {
e = encoder
cmd.Env = []string{
// passed through to shim by hsu
shimEnv + "=" + strconv.Itoa(fd),
// interpreted by hsu
"HAKUREI_APP_ID=" + seal.user.aid.String(),
}
}
if len(seal.user.supp) > 0 {
hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp)
// interpreted by hsu
cmd.Env = append(cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " "))
}
hlog.Verbosef("setuid helper at %s", hsuPath)
hlog.Suspend()
if err := cmd.Start(); err != nil {
return hlog.WrapErrSuffix(err,
"cannot start setuid wrapper:")
}
rs.SetStart()
// this prevents blocking forever on an early failure
waitErr, setupErr := make(chan error, 1), make(chan error, 1)
go func() { waitErr <- cmd.Wait(); cancel() }()
go func() {
setupErr <- e.Encode(&shimParams{
os.Getpid(),
seal.waitDelay,
seal.container,
hlog.Load(),
})
}()
select {
case err := <-setupErr:
if err != nil {
hlog.Resume()
return hlog.WrapErrSuffix(err,
"cannot transmit shim config:")
}
case <-ctx.Done():
hlog.Resume()
return hlog.WrapErr(syscall.ECANCELED,
"shim setup canceled")
}
// returned after blocking on waitErr
var earlyStoreErr = new(StateStoreError)
{
// shim accepted setup payload, create process state
sd := state.State{
ID: seal.id.unwrap(),
PID: cmd.Process.Pid,
Time: *rs.Time,
}
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
})
}
// state in store at this point, destroy defunct state entry on return
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
waitTimeout := make(chan struct{})
go func() { <-seal.ctx.Done(); time.Sleep(shimWaitTimeout); close(waitTimeout) }()
select {
case rs.WaitErr = <-waitErr:
rs.WaitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus)
if hlog.Load() {
switch {
case rs.Exited():
hlog.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus())
case rs.CoreDump():
hlog.Verbosef("process %d dumped core", cmd.Process.Pid)
case rs.Signaled():
hlog.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal())
default:
hlog.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus)
}
}
case <-waitTimeout:
rs.WaitErr = syscall.ETIMEDOUT
hlog.Resume()
log.Printf("process %d did not terminate", cmd.Process.Pid)
}
hlog.Resume()
if seal.sync != nil {
if err := seal.sync.Close(); err != nil {
log.Printf("cannot close wayland security context: %v", err)
}
}
if seal.dbusMsg != nil {
seal.dbusMsg()
}
return earlyStoreErr.equiv("cannot save process state:")
}

View File

@ -9,7 +9,8 @@ import (
"io" "io"
"io/fs" "io/fs"
"os" "os"
"os/user" "path"
"regexp"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@ -19,20 +20,49 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
"hakurei.app/system" "hakurei.app/system"
"hakurei.app/system/acl" "hakurei.app/system/acl"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
"hakurei.app/system/wayland" "hakurei.app/system/wayland"
) )
func newWithMessage(msg string) error { return newWithMessageError(msg, os.ErrInvalid) } const (
func newWithMessageError(msg string, err error) error { home = "HOME"
return &hst.AppError{Step: "finalise", Err: err, Msg: msg} shell = "SHELL"
}
// An outcome is the runnable state of a hakurei container via [hst.Config]. xdgConfigHome = "XDG_CONFIG_HOME"
xdgRuntimeDir = "XDG_RUNTIME_DIR"
xdgSessionClass = "XDG_SESSION_CLASS"
xdgSessionType = "XDG_SESSION_TYPE"
term = "TERM"
display = "DISPLAY"
pulseServer = "PULSE_SERVER"
pulseCookie = "PULSE_COOKIE"
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
)
var (
ErrIdent = errors.New("invalid identity")
ErrName = errors.New("invalid username")
ErrXDisplay = errors.New(display + " unset")
ErrPulseCookie = errors.New("pulse cookie not present")
ErrPulseSocket = errors.New("pulse socket not present")
ErrPulseMode = errors.New("unexpected pulse socket mode")
)
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$")
// outcome stores copies of various parts of [hst.Config]
type outcome struct { type outcome struct {
// copied from initialising [app] // copied from initialising [app]
id *stringPair[state.ID] id *stringPair[state.ID]
@ -53,9 +83,8 @@ type outcome struct {
container *container.Params container *container.Params
env map[string]string env map[string]string
sync *os.File sync *os.File
active atomic.Bool
syscallDispatcher f atomic.Bool
} }
// shareHost holds optional share directory state that must not be accessed directly // shareHost holds optional share directory state that must not be accessed directly
@ -107,7 +136,8 @@ func (share *shareHost) runtime() *container.Absolute {
// hsuUser stores post-hsu credentials and metadata // hsuUser stores post-hsu credentials and metadata
type hsuUser struct { type hsuUser struct {
identity *stringPair[int] // identity
aid *stringPair[int]
// target uid resolved by hid:aid // target uid resolved by hid:aid
uid *stringPair[int] uid *stringPair[int]
@ -120,82 +150,59 @@ type hsuUser struct {
username string username string
} }
func (k *outcome) finalise(ctx context.Context, config *hst.Config) error { func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error {
const ( if seal.ctx != nil {
home = "HOME" panic("finalise called twice")
shell = "SHELL"
xdgConfigHome = "XDG_CONFIG_HOME"
xdgRuntimeDir = "XDG_RUNTIME_DIR"
xdgSessionClass = "XDG_SESSION_CLASS"
xdgSessionType = "XDG_SESSION_TYPE"
term = "TERM"
display = "DISPLAY"
pulseServer = "PULSE_SERVER"
pulseCookie = "PULSE_COOKIE"
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
)
if ctx == nil {
// unreachable
panic("invalid call to finalise")
} }
if k.ctx != nil { seal.ctx = ctx
// unreachable
panic("attempting to finalise twice")
}
k.ctx = ctx
if config == nil { if config == nil {
return newWithMessage("invalid configuration") return hlog.WrapErr(syscall.EINVAL, syscall.EINVAL.Error())
} }
if config.Home == nil { if config.Home == nil {
return newWithMessage("invalid path to home directory") return hlog.WrapErr(os.ErrInvalid, "invalid path to home directory")
} }
{ {
// encode initial configuration for state tracking // encode initial configuration for state tracking
ct := new(bytes.Buffer) ct := new(bytes.Buffer)
if err := gob.NewEncoder(ct).Encode(config); err != nil { if err := gob.NewEncoder(ct).Encode(config); err != nil {
return &hst.AppError{Step: "encode initial config", Err: err} return hlog.WrapErrSuffix(err,
"cannot encode initial config:")
} }
k.ct = ct seal.ct = ct
} }
// allowed identity range 0 to 9999, this is checked again in hsu // allowed aid range 0 to 9999, this is checked again in hsu
if config.Identity < 0 || config.Identity > 9999 { if config.Identity < 0 || config.Identity > 9999 {
return newWithMessage(fmt.Sprintf("identity %d out of range", config.Identity)) return hlog.WrapErr(ErrIdent,
fmt.Sprintf("identity %d out of range", config.Identity))
} }
k.user = hsuUser{ seal.user = hsuUser{
identity: newInt(config.Identity), aid: newInt(config.Identity),
home: config.Home, home: config.Home,
username: config.Username, username: config.Username,
} }
if seal.user.username == "" {
hsu := Hsu{k: k} seal.user.username = "chronos"
if k.user.username == "" { } else if !posixUsername.MatchString(seal.user.username) ||
k.user.username = "chronos" len(seal.user.username) >= internal.Sysconf(internal.SC_LOGIN_NAME_MAX) {
} else if !isValidUsername(k.user.username) { return hlog.WrapErr(ErrName,
return newWithMessage(fmt.Sprintf("invalid user name %q", k.user.username)) fmt.Sprintf("invalid user name %q", seal.user.username))
} }
k.user.uid = newInt(HsuUid(hsu.MustID(), k.user.identity.unwrap())) if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil {
return err
k.user.supp = make([]string, len(config.Groups)) } else {
seal.user.uid = newInt(u)
}
seal.user.supp = make([]string, len(config.Groups))
for i, name := range config.Groups { for i, name := range config.Groups {
if gid, err := k.lookupGroupId(name); err != nil { if g, err := sys.LookupGroup(name); err != nil {
var unknownGroupError user.UnknownGroupError return hlog.WrapErr(err,
if errors.As(err, &unknownGroupError) { fmt.Sprintf("unknown group %q", name))
return newWithMessageError(fmt.Sprintf("unknown group %q", name), unknownGroupError)
} else {
return &hst.AppError{Step: "look up group by name", Err: err}
}
} else { } else {
k.user.supp[i] = gid seal.user.supp[i] = g.Gid
} }
} }
@ -205,7 +212,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
if config.Shell == nil { if config.Shell == nil {
config.Shell = container.AbsFHSRoot.Append("bin", "sh") config.Shell = container.AbsFHSRoot.Append("bin", "sh")
s, _ := k.lookupEnv(shell) s, _ := sys.LookupEnv(shell)
if a, err := container.NewAbs(s); err == nil { if a, err := container.NewAbs(s); err == nil {
config.Shell = a config.Shell = a
} }
@ -214,10 +221,10 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
// hsu clears the environment so resolve paths early // hsu clears the environment so resolve paths early
if config.Path == nil { if config.Path == nil {
if len(config.Args) > 0 { if len(config.Args) > 0 {
if p, err := k.lookPath(config.Args[0]); err != nil { if p, err := sys.LookPath(config.Args[0]); err != nil {
return &hst.AppError{Step: "look up executable file", Err: err} return hlog.WrapErr(err, err.Error())
} else if config.Path, err = container.NewAbs(p); err != nil { } else if config.Path, err = container.NewAbs(p); err != nil {
return newWithMessageError(err.Error(), err) return hlog.WrapErr(err, err.Error())
} }
} else { } else {
config.Path = config.Shell config.Path = config.Shell
@ -232,7 +239,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
// autoroot, includes the home directory // autoroot, includes the home directory
{FilesystemConfig: &hst.FSBind{ {&hst.FSBind{
Target: container.AbsFHSRoot, Target: container.AbsFHSRoot,
Source: container.AbsFHSRoot, Source: container.AbsFHSRoot,
Write: true, Write: true,
@ -250,7 +257,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
// hide nscd from container if present // hide nscd from container if present
nscd := container.AbsFHSVar.Append("run/nscd") nscd := container.AbsFHSVar.Append("run/nscd")
if _, err := k.stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) { if _, err := sys.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) {
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}}) conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}})
} }
@ -268,95 +275,93 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
// late nil checks for pd behaviour // late nil checks for pd behaviour
if config.Shell == nil { if config.Shell == nil {
return newWithMessage("invalid shell path") return hlog.WrapErr(syscall.EINVAL, "invalid shell path")
} }
if config.Path == nil { if config.Path == nil {
return newWithMessage("invalid program path") return hlog.WrapErr(syscall.EINVAL, "invalid program path")
} }
// TODO(ophestra): revert this after params to shim
share := &shareHost{seal: k}
copyPaths(k.syscallDispatcher, &share.sc, hsu.MustID())
var mapuid, mapgid *stringPair[int] var mapuid, mapgid *stringPair[int]
{ {
var uid, gid int var uid, gid int
var err error var err error
k.container, k.env, err = newContainer(k, config.Container, k.id.String(), &share.sc, &uid, &gid) seal.container, seal.env, err = newContainer(config.Container, sys, seal.id.String(), &uid, &gid)
k.waitDelay = config.Container.WaitDelay seal.waitDelay = config.Container.WaitDelay
if err != nil { if err != nil {
return &hst.AppError{Step: "initialise container configuration", Err: err} return hlog.WrapErrSuffix(err,
"cannot initialise container configuration:")
} }
if len(config.Args) == 0 { if len(config.Args) == 0 {
config.Args = []string{config.Path.String()} config.Args = []string{config.Path.String()}
} }
k.container.Path = config.Path seal.container.Path = config.Path
k.container.Args = config.Args seal.container.Args = config.Args
mapuid = newInt(uid) mapuid = newInt(uid)
mapgid = newInt(gid) mapgid = newInt(gid)
if k.env == nil { if seal.env == nil {
k.env = make(map[string]string, 1<<6) seal.env = make(map[string]string, 1<<6)
} }
} }
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid // inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String()) innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String())
k.env[xdgRuntimeDir] = innerRuntimeDir.String() seal.env[xdgRuntimeDir] = innerRuntimeDir.String()
k.env[xdgSessionClass] = "user" seal.env[xdgSessionClass] = "user"
k.env[xdgSessionType] = "tty" seal.env[xdgSessionType] = "tty"
k.runDirPath = share.sc.RunDirPath share := &shareHost{seal: seal, sc: sys.Paths()}
k.sys = system.New(k.ctx, k.user.uid.unwrap()) seal.runDirPath = share.sc.RunDirPath
k.sys.Ensure(share.sc.SharePath.String(), 0711) seal.sys = system.New(seal.user.uid.unwrap())
seal.sys.Ensure(share.sc.SharePath.String(), 0711)
{ {
runtimeDir := share.sc.SharePath.Append("runtime") runtimeDir := share.sc.SharePath.Append("runtime")
k.sys.Ensure(runtimeDir.String(), 0700) seal.sys.Ensure(runtimeDir.String(), 0700)
k.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute) seal.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute)
runtimeDirInst := runtimeDir.Append(k.user.identity.String()) runtimeDirInst := runtimeDir.Append(seal.user.aid.String())
k.sys.Ensure(runtimeDirInst.String(), 0700) seal.sys.Ensure(runtimeDirInst.String(), 0700)
k.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute)
k.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755) seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
k.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable) seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
} }
{ {
tmpdir := share.sc.SharePath.Append("tmpdir") tmpdir := share.sc.SharePath.Append("tmpdir")
k.sys.Ensure(tmpdir.String(), 0700) seal.sys.Ensure(tmpdir.String(), 0700)
k.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute) seal.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute)
tmpdirInst := tmpdir.Append(k.user.identity.String()) tmpdirInst := tmpdir.Append(seal.user.aid.String())
k.sys.Ensure(tmpdirInst.String(), 01700) seal.sys.Ensure(tmpdirInst.String(), 01700)
k.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute)
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp // mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
k.container.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable) seal.container.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable)
} }
{ {
username := "chronos" username := "chronos"
if k.user.username != "" { if seal.user.username != "" {
username = k.user.username username = seal.user.username
} }
k.container.Dir = k.user.home seal.container.Dir = seal.user.home
k.env["HOME"] = k.user.home.String() seal.env["HOME"] = seal.user.home.String()
k.env["USER"] = username seal.env["USER"] = username
k.env[shell] = config.Shell.String() seal.env[shell] = config.Shell.String()
k.container.Place(container.AbsFHSEtc.Append("passwd"), seal.container.Place(container.AbsFHSEtc.Append("passwd"),
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+k.user.home.String()+":"+config.Shell.String()+"\n")) []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+seal.user.home.String()+":"+config.Shell.String()+"\n"))
k.container.Place(container.AbsFHSEtc.Append("group"), seal.container.Place(container.AbsFHSEtc.Append("group"),
[]byte("hakurei:x:"+mapgid.String()+":\n")) []byte("hakurei:x:"+mapgid.String()+":\n"))
} }
// pass TERM for proper terminal I/O in initial process // pass TERM for proper terminal I/O in initial process
if t, ok := k.lookupEnv(term); ok { if t, ok := sys.LookupEnv(term); ok {
k.env[term] = t seal.env[term] = t
} }
if config.Enablements.Unwrap()&system.EWayland != 0 { if config.Enablements.Unwrap()&system.EWayland != 0 {
// outer wayland socket (usually `/run/user/%d/wayland-%d`) // outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath *container.Absolute var socketPath *container.Absolute
if name, ok := k.lookupEnv(wayland.WaylandDisplay); !ok { if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok {
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName) hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
socketPath = share.sc.RuntimePath.Append(wayland.FallbackName) socketPath = share.sc.RuntimePath.Append(wayland.FallbackName)
} else if a, err := container.NewAbs(name); err != nil { } else if a, err := container.NewAbs(name); err != nil {
@ -366,29 +371,30 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
} }
innerPath := innerRuntimeDir.Append(wayland.FallbackName) innerPath := innerRuntimeDir.Append(wayland.FallbackName)
k.env[wayland.WaylandDisplay] = wayland.FallbackName seal.env[wayland.WaylandDisplay] = wayland.FallbackName
if !config.DirectWayland { // set up security-context-v1 if !config.DirectWayland { // set up security-context-v1
appID := config.ID appID := config.ID
if appID == "" { if appID == "" {
// use instance ID in case app id is not set // use instance ID in case app id is not set
appID = "app.hakurei." + k.id.String() appID = "app.hakurei." + seal.id.String()
} }
// downstream socket paths // downstream socket paths
outerPath := share.instance().Append("wayland") outerPath := share.instance().Append("wayland")
k.sys.Wayland(&k.sync, outerPath.String(), socketPath.String(), appID, k.id.String()) seal.sys.Wayland(&seal.sync, outerPath.String(), socketPath.String(), appID, seal.id.String())
k.container.Bind(outerPath, innerPath, 0) seal.container.Bind(outerPath, innerPath, 0)
} else { // bind mount wayland socket (insecure) } else { // bind mount wayland socket (insecure)
hlog.Verbose("direct wayland access, PROCEED WITH CAUTION") hlog.Verbose("direct wayland access, PROCEED WITH CAUTION")
share.ensureRuntimeDir() share.ensureRuntimeDir()
k.container.Bind(socketPath, innerPath, 0) seal.container.Bind(socketPath, innerPath, 0)
k.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute)
} }
} }
if config.Enablements.Unwrap()&system.EX11 != 0 { if config.Enablements.Unwrap()&system.EX11 != 0 {
if d, ok := k.lookupEnv(display); !ok { if d, ok := sys.LookupEnv(display); !ok {
return newWithMessage("DISPLAY is not set") return hlog.WrapErr(ErrXDisplay,
"DISPLAY is not set")
} else { } else {
socketDir := container.AbsFHSTmp.Append(".X11-unix") socketDir := container.AbsFHSTmp.Append(".X11-unix")
@ -405,21 +411,20 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
} }
} }
if socketPath != nil { if socketPath != nil {
if _, err := k.stat(socketPath.String()); err != nil { if _, err := sys.Stat(socketPath.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err} return hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access X11 socket %q:", socketPath))
} }
} else { } else {
k.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute)
if !config.Container.HostAbstract { d = "unix:" + socketPath.String()
d = "unix:" + socketPath.String()
}
} }
} }
k.sys.ChangeHosts("#" + k.user.uid.String()) seal.sys.ChangeHosts("#" + seal.user.uid.String())
k.env[display] = d seal.env[display] = d
k.container.Bind(socketDir, socketDir, 0) seal.container.Bind(socketDir, socketDir, 0)
} }
} }
@ -429,101 +434,46 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
// PulseAudio socket (usually `/run/user/%d/pulse/native`) // PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := pulseRuntimeDir.Append("native") pulseSocket := pulseRuntimeDir.Append("native")
if _, err := k.stat(pulseRuntimeDir.String()); err != nil { if _, err := sys.Stat(pulseRuntimeDir.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err} return hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir))
} }
return newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir)) return hlog.WrapErr(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
} }
if s, err := k.stat(pulseSocket.String()); err != nil { if s, err := sys.Stat(pulseSocket.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err} return hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket))
} }
return newWithMessage(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir)) return hlog.WrapErr(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
} else { } else {
if m := s.Mode(); m&0o006 != 0o006 { if m := s.Mode(); m&0o006 != 0o006 {
return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m)) return hlog.WrapErr(ErrPulseMode,
fmt.Sprintf("unexpected permissions on %q:", pulseSocket), m)
} }
} }
// hard link pulse socket into target-executable share // hard link pulse socket into target-executable share
innerPulseRuntimeDir := share.runtime().Append("pulse") innerPulseRuntimeDir := share.runtime().Append("pulse")
innerPulseSocket := innerRuntimeDir.Append("pulse", "native") innerPulseSocket := innerRuntimeDir.Append("pulse", "native")
k.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String()) seal.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String())
k.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
k.env[pulseServer] = "unix:" + innerPulseSocket.String() seal.env[pulseServer] = "unix:" + innerPulseSocket.String()
// publish current user's pulse cookie for target user // publish current user's pulse cookie for target user
var paCookiePath *container.Absolute if src, err := discoverPulseCookie(sys); err != nil {
{ // not fatal
const paLocateStep = "locate PulseAudio cookie" hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message()))
// from environment
if p, ok := k.lookupEnv(pulseCookie); ok {
if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err}
} else {
// this takes precedence, do not verify whether the file is accessible
paCookiePath = a
goto out
}
}
// $HOME/.pulse-cookie
if p, ok := k.lookupEnv(home); ok {
if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err}
} else {
paCookiePath = a.Append(".pulse-cookie")
}
if s, err := k.stat(paCookiePath.String()); err != nil {
paCookiePath = nil
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
}
// fallthrough
} else if s.IsDir() {
paCookiePath = nil
} else {
goto out
}
}
// $XDG_CONFIG_HOME/pulse/cookie
if p, ok := k.lookupEnv(xdgConfigHome); ok {
if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err}
} else {
paCookiePath = a.Append("pulse", "cookie")
}
if s, err := k.stat(paCookiePath.String()); err != nil {
paCookiePath = nil
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
}
// fallthrough
} else if s.IsDir() {
paCookiePath = nil
} else {
goto out
}
}
out:
}
if paCookiePath != nil {
innerDst := hst.AbsTmp.Append("/pulse-cookie")
k.env[pulseCookie] = innerDst.String()
var payload *[]byte
k.container.PlaceP(innerDst, &payload)
k.sys.CopyFile(payload, paCookiePath.String(), 256, 256)
} else { } else {
hlog.Verbose("cannot locate PulseAudio cookie (tried " + innerDst := hst.AbsTmp.Append("/pulse-cookie")
"$PULSE_COOKIE, " + seal.env[pulseCookie] = innerDst.String()
"$XDG_CONFIG_HOME/pulse/cookie, " + var payload *[]byte
"$HOME/.pulse-cookie)") seal.container.PlaceP(innerDst, &payload)
seal.sys.CopyFile(payload, src, 256, 256)
} }
} }
@ -537,30 +487,30 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket") sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket")
// configure dbus proxy // configure dbus proxy
if f, err := k.sys.ProxyDBus( if f, err := seal.sys.ProxyDBus(
config.SessionBus, config.SystemBus, config.SessionBus, config.SystemBus,
sessionPath.String(), systemPath.String(), sessionPath.String(), systemPath.String(),
); err != nil { ); err != nil {
return err return err
} else { } else {
k.dbusMsg = f seal.dbusMsg = f
} }
// share proxy sockets // share proxy sockets
sessionInner := innerRuntimeDir.Append("bus") sessionInner := innerRuntimeDir.Append("bus")
k.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String() seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String()
k.container.Bind(sessionPath, sessionInner, 0) seal.container.Bind(sessionPath, sessionInner, 0)
k.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write) seal.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write)
if config.SystemBus != nil { if config.SystemBus != nil {
systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket") systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket")
k.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String() seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String()
k.container.Bind(systemPath, systemInner, 0) seal.container.Bind(systemPath, systemInner, 0)
k.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write) seal.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write)
} }
} }
// mount root read-only as the final setup Op // mount root read-only as the final setup Op
k.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY) seal.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY)
// append ExtraPerms last // append ExtraPerms last
for _, p := range config.ExtraPerms { for _, p := range config.ExtraPerms {
@ -569,7 +519,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
} }
if p.Ensure { if p.Ensure {
k.sys.Ensure(p.Path.String(), 0700) seal.sys.Ensure(p.Path.String(), 0700)
} }
perms := make(acl.Perms, 0, 3) perms := make(acl.Perms, 0, 3)
@ -582,24 +532,63 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
if p.Execute { if p.Execute {
perms = append(perms, acl.Execute) perms = append(perms, acl.Execute)
} }
k.sys.UpdatePermType(system.User, p.Path.String(), perms...) seal.sys.UpdatePermType(system.User, p.Path.String(), perms...)
} }
// flatten and sort env for deterministic behaviour // flatten and sort env for deterministic behaviour
k.container.Env = make([]string, 0, len(k.env)) seal.container.Env = make([]string, 0, len(seal.env))
for key, value := range k.env { for k, v := range seal.env {
if strings.IndexByte(key, '=') != -1 { if strings.IndexByte(k, '=') != -1 {
return &hst.AppError{Step: "flatten environment", Err: syscall.EINVAL, return hlog.WrapErr(syscall.EINVAL,
Msg: fmt.Sprintf("invalid environment variable %s", key)} fmt.Sprintf("invalid environment variable %s", k))
} }
k.container.Env = append(k.container.Env, key+"="+value) seal.container.Env = append(seal.container.Env, k+"="+v)
} }
slices.Sort(k.container.Env) slices.Sort(seal.container.Env)
if hlog.Load() { if hlog.Load() {
hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d", hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
k.user.uid, k.user.username, config.Groups, k.container.Args, len(*k.container.Ops)) seal.user.uid, seal.user.username, config.Groups, seal.container.Args, len(*seal.container.Ops))
} }
return nil return nil
} }
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie(sys sys.State) (string, error) {
if p, ok := sys.LookupEnv(pulseCookie); ok {
return p, nil
}
// dotfile $HOME/.pulse-cookie
if p, ok := sys.LookupEnv(home); ok {
p = path.Join(p, ".pulse-cookie")
if s, err := sys.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return p, hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
}
// not found, try next method
} else if !s.IsDir() {
return p, nil
}
}
// $XDG_CONFIG_HOME/pulse/cookie
if p, ok := sys.LookupEnv(xdgConfigHome); ok {
p = path.Join(p, "pulse", "cookie")
if s, err := sys.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return p, hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
}
// not found, try next method
} else if !s.IsDir() {
return p, nil
}
}
return "", hlog.WrapErr(ErrPulseCookie,
fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
pulseCookie, xdgConfigHome, home))
}

View File

@ -45,10 +45,8 @@ const (
// ShimExitOrphan is returned when the shim is orphaned before monitor delivers a signal. // ShimExitOrphan is returned when the shim is orphaned before monitor delivers a signal.
ShimExitOrphan = 3 ShimExitOrphan = 3
// DefaultShimWaitDelay is used when WaitDelay has its zero value.
DefaultShimWaitDelay = 5 * time.Second DefaultShimWaitDelay = 5 * time.Second
// MaxShimWaitDelay is used instead if WaitDelay exceeds its value. MaxShimWaitDelay = 30 * time.Second
MaxShimWaitDelay = 30 * time.Second
) )
// ShimMain is the main function of the shim process and runs as the unconstrained target user. // ShimMain is the main function of the shim process and runs as the unconstrained target user.
@ -67,7 +65,7 @@ func ShimMain() {
if errors.Is(err, syscall.EBADF) { if errors.Is(err, syscall.EBADF) {
log.Fatal("invalid config descriptor") log.Fatal("invalid config descriptor")
} }
if errors.Is(err, container.ErrReceiveEnv) { if errors.Is(err, container.ErrNotSet) {
log.Fatal("HAKUREI_SHIM not set") log.Fatal("HAKUREI_SHIM not set")
} }
@ -157,11 +155,11 @@ func ShimMain() {
} }
if err := z.Start(); err != nil { if err := z.Start(); err != nil {
printMessageError("cannot start container:", err) hlog.PrintBaseError(err, "cannot start container:")
os.Exit(1) os.Exit(1)
} }
if err := z.Serve(); err != nil { if err := z.Serve(); err != nil {
printMessageError("cannot configure container:", err) hlog.PrintBaseError(err, "cannot configure container:")
} }
if err := seccomp.Load( if err := seccomp.Load(

View File

@ -27,27 +27,27 @@ type multiStore struct {
lock sync.RWMutex lock sync.RWMutex
} }
func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) { func (s *multiStore) Do(aid int, f func(c Cursor)) (bool, error) {
s.lock.RLock() s.lock.RLock()
defer s.lock.RUnlock() defer s.lock.RUnlock()
// load or initialise new backend // load or initialise new backend
b := new(multiBackend) b := new(multiBackend)
b.lock.Lock() b.lock.Lock()
if v, ok := s.backends.LoadOrStore(identity, b); ok { if v, ok := s.backends.LoadOrStore(aid, b); ok {
b = v.(*multiBackend) b = v.(*multiBackend)
} else { } else {
b.path = path.Join(s.base, strconv.Itoa(identity)) b.path = path.Join(s.base, strconv.Itoa(aid))
// ensure directory // ensure directory
if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) { if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
s.backends.CompareAndDelete(identity, b) s.backends.CompareAndDelete(aid, b)
return false, err return false, err
} }
// open locker file // open locker file
if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil { if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
s.backends.CompareAndDelete(identity, b) s.backends.CompareAndDelete(aid, b)
return false, err return false, err
} else { } else {
b.lockfile = l b.lockfile = l

View File

@ -17,7 +17,7 @@ type Store interface {
// Do calls f exactly once and ensures store exclusivity until f returns. // Do calls f exactly once and ensures store exclusivity until f returns.
// Returns whether f is called and any errors during the locking process. // Returns whether f is called and any errors during the locking process.
// Cursor provided to f becomes invalid as soon as f returns. // Cursor provided to f becomes invalid as soon as f returns.
Do(identity int, f func(c Cursor)) (ok bool, err error) Do(aid int, f func(c Cursor)) (ok bool, err error)
// List queries the store and returns a list of aids known to the store. // List queries the store and returns a list of aids known to the store.
// Note that some or all returned aids might not have any active apps. // Note that some or all returned aids might not have any active apps.

View File

@ -2,9 +2,12 @@ package app
import ( import (
"strconv" "strconv"
"hakurei.app/internal/app/state"
) )
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} } func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
func newID(id *state.ID) *stringPair[state.ID] { return &stringPair[state.ID]{*id, id.String()} }
// stringPair stores a value and its string representation. // stringPair stores a value and its string representation.
type stringPair[T comparable] struct { type stringPair[T comparable] struct {

View File

@ -1,8 +0,0 @@
package app
//#include <unistd.h>
import "C"
const _SC_LOGIN_NAME_MAX = C._SC_LOGIN_NAME_MAX
func sysconf(name C.int) int { return int(C.sysconf(name)) }

View File

@ -1,12 +0,0 @@
package app
import "regexp"
// nameRegex is the default NAME_REGEX value from adduser.
var nameRegex = regexp.MustCompilePOSIX(`^[a-zA-Z][a-zA-Z0-9_-]*\$?$`)
// isValidUsername returns whether the argument is a valid username
func isValidUsername(username string) bool {
return len(username) < sysconf(_SC_LOGIN_NAME_MAX) &&
nameRegex.MatchString(username)
}

81
internal/hlog/errors.go Normal file
View File

@ -0,0 +1,81 @@
package hlog
import (
"fmt"
"log"
"reflect"
"strings"
)
// baseError implements a basic error container
type baseError struct {
Err error
}
func (e *baseError) Error() string { return e.Err.Error() }
func (e *baseError) Unwrap() error { return e.Err }
// BaseError implements an error container with a user-facing message
type BaseError struct {
message string
baseError
}
// Message returns a user-facing error message
func (e *BaseError) Message() string { return e.message }
// WrapErr wraps an error with a corresponding message.
func WrapErr(err error, a ...any) error {
if err == nil {
return nil
}
return wrapErr(err, fmt.Sprintln(a...))
}
// WrapErrSuffix wraps an error with a corresponding message with err at the end of the message.
func WrapErrSuffix(err error, a ...any) error {
if err == nil {
return nil
}
return wrapErr(err, fmt.Sprintln(append(a, err)...))
}
// WrapErrFunc wraps an error with a corresponding message returned by f.
func WrapErrFunc(err error, f func(err error) string) error {
if err == nil {
return nil
}
return wrapErr(err, f(err))
}
func wrapErr(err error, message string) *BaseError {
return &BaseError{message, baseError{err}}
}
var (
baseErrorType = reflect.TypeFor[*BaseError]()
)
func AsBaseError(err error, target **BaseError) bool {
v := reflect.ValueOf(err)
if !v.CanConvert(baseErrorType) {
return false
}
*target = v.Convert(baseErrorType).Interface().(*BaseError)
return true
}
func PrintBaseError(err error, fallback string) {
var e *BaseError
if AsBaseError(err, &e) {
if msg := e.Message(); strings.TrimSpace(msg) != "" {
log.Print(msg)
return
}
Verbose("*"+fallback, err)
return
}
log.Println(fallback, err)
}

View File

@ -2,17 +2,69 @@
package hlog package hlog
import ( import (
"bytes"
"io"
"log" "log"
"os" "os"
"sync"
"hakurei.app/container" "sync/atomic"
"syscall"
) )
var o = &container.Suspendable{Downstream: os.Stderr} const (
bufSize = 4 * 1024
bufSizeMax = 16 * 1024 * 1024
)
var o = &suspendable{w: os.Stderr}
// Prepare configures the system logger for [Suspend] and [Resume] to take effect. // Prepare configures the system logger for [Suspend] and [Resume] to take effect.
func Prepare(prefix string) { log.SetPrefix(prefix + ": "); log.SetFlags(0); log.SetOutput(o) } func Prepare(prefix string) { log.SetPrefix(prefix + ": "); log.SetFlags(0); log.SetOutput(o) }
type suspendable struct {
w io.Writer
s atomic.Bool
buf bytes.Buffer
bufOnce sync.Once
bufMu sync.Mutex
dropped int
}
func (s *suspendable) Write(p []byte) (n int, err error) {
if !s.s.Load() {
return s.w.Write(p)
}
s.bufOnce.Do(func() { s.prepareBuf() })
s.bufMu.Lock()
defer s.bufMu.Unlock()
if l := len(p); s.buf.Len()+l > bufSizeMax {
s.dropped += l
return 0, syscall.ENOMEM
}
return s.buf.Write(p)
}
func (s *suspendable) prepareBuf() { s.buf.Grow(bufSize) }
func (s *suspendable) Suspend() bool { return o.s.CompareAndSwap(false, true) }
func (s *suspendable) Resume() (resumed bool, dropped uintptr, n int64, err error) {
if o.s.CompareAndSwap(true, false) {
o.bufMu.Lock()
defer o.bufMu.Unlock()
resumed = true
dropped = uintptr(o.dropped)
o.dropped = 0
n, err = io.Copy(s.w, &s.buf)
s.buf = bytes.Buffer{}
s.prepareBuf()
}
return
}
func Suspend() bool { return o.Suspend() } func Suspend() bool { return o.Suspend() }
func Resume() bool { func Resume() bool {
resumed, dropped, _, err := o.Resume() resumed, dropped, _, err := o.Resume()

View File

@ -2,9 +2,11 @@ package hlog
type Output struct{} type Output struct{}
func (Output) IsVerbose() bool { return Load() } func (Output) IsVerbose() bool { return Load() }
func (Output) Verbose(v ...any) { Verbose(v...) } func (Output) Verbose(v ...any) { Verbose(v...) }
func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) } func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) }
func (Output) Suspend() { Suspend() } func (Output) WrapErr(err error, a ...any) error { return WrapErr(err, a...) }
func (Output) Resume() bool { return Resume() } func (Output) PrintBaseErr(err error, fallback string) { PrintBaseError(err, fallback) }
func (Output) BeforeExit() { BeforeExit() } func (Output) Suspend() { Suspend() }
func (Output) Resume() bool { return Resume() }
func (Output) BeforeExit() { BeforeExit() }

82
internal/sys/interface.go Normal file
View File

@ -0,0 +1,82 @@
// Package sys wraps OS interaction library functions.
package sys
import (
"io/fs"
"log"
"os/user"
"strconv"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
// State provides safe interaction with operating system state.
type State interface {
// Getuid provides [os.Getuid].
Getuid() int
// Getgid provides [os.Getgid].
Getgid() int
// LookupEnv provides [os.LookupEnv].
LookupEnv(key string) (string, bool)
// TempDir provides [os.TempDir].
TempDir() string
// LookPath provides exec.LookPath.
LookPath(file string) (string, error)
// MustExecutable provides [container.MustExecutable].
MustExecutable() string
// LookupGroup provides [user.LookupGroup].
LookupGroup(name string) (*user.Group, error)
// ReadDir provides [os.ReadDir].
ReadDir(name string) ([]fs.DirEntry, error)
// Stat provides [os.Stat].
Stat(name string) (fs.FileInfo, error)
// Open provides [os.Open].
Open(name string) (fs.File, error)
// EvalSymlinks provides filepath.EvalSymlinks.
EvalSymlinks(path string) (string, error)
// Exit provides [os.Exit].
Exit(code int)
Println(v ...any)
Printf(format string, v ...any)
// Paths returns a populated [hst.Paths] struct.
Paths() hst.Paths
// Uid invokes hsu and returns target uid.
// Any errors returned by Uid is already wrapped [hlog.BaseError].
Uid(identity int) (int, error)
}
// GetUserID obtains user id from hsu by querying uid of identity 0.
func GetUserID(os State) (int, error) {
if uid, err := os.Uid(0); err != nil {
return -1, err
} else {
return (uid / 10000) - 100, nil
}
}
// CopyPaths is a generic implementation of [hst.Paths].
func CopyPaths(os State, v *hst.Paths, userid int) {
if tempDir, err := container.NewAbs(os.TempDir()); err != nil {
log.Fatalf("invalid TMPDIR: %v", err)
} else {
v.TempDir = tempDir
}
v.SharePath = v.TempDir.Append("hakurei." + strconv.Itoa(userid))
hlog.Verbosef("process share directory at %q", v.SharePath)
r, _ := os.LookupEnv(xdgRuntimeDir)
if a, err := container.NewAbs(r); err != nil {
// fall back to path in share since hakurei has no hard XDG dependency
v.RunDirPath = v.SharePath.Append("run")
v.RuntimePath = v.RunDirPath.Append("compat")
} else {
v.RuntimePath = a
v.RunDirPath = v.RuntimePath.Append("hakurei")
}
hlog.Verbosef("runtime directory at %q", v.RunDirPath)
}

114
internal/sys/std.go Normal file
View File

@ -0,0 +1,114 @@
package sys
import (
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"sync"
"syscall"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
// Std implements System using the standard library.
type Std struct {
paths hst.Paths
pathsOnce sync.Once
uidOnce sync.Once
uidCopy map[int]struct {
uid int
err error
}
uidMu sync.RWMutex
}
func (s *Std) Getuid() int { return os.Getuid() }
func (s *Std) Getgid() int { return os.Getgid() }
func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
func (s *Std) TempDir() string { return os.TempDir() }
func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }
func (s *Std) MustExecutable() string { return container.MustExecutable() }
func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) }
func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
func (s *Std) Open(name string) (fs.File, error) { return os.Open(name) }
func (s *Std) EvalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) }
func (s *Std) Exit(code int) { internal.Exit(code) }
func (s *Std) Println(v ...any) { hlog.Verbose(v...) }
func (s *Std) Printf(format string, v ...any) { hlog.Verbosef(format, v...) }
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) Paths() hst.Paths {
s.pathsOnce.Do(func() {
if userid, err := GetUserID(s); err != nil {
hlog.PrintBaseError(err, "cannot obtain user id from hsu:")
hlog.BeforeExit()
s.Exit(1)
} else {
CopyPaths(s, &s.paths, userid)
}
})
return s.paths
}
func (s *Std) Uid(identity int) (int, error) {
s.uidOnce.Do(func() {
s.uidCopy = make(map[int]struct {
uid int
err error
})
})
{
s.uidMu.RLock()
u, ok := s.uidCopy[identity]
s.uidMu.RUnlock()
if ok {
return u.uid, u.err
}
}
s.uidMu.Lock()
defer s.uidMu.Unlock()
u := struct {
uid int
err error
}{}
defer func() { s.uidCopy[identity] = u }()
u.uid = -1
hsuPath := internal.MustHsuPath()
cmd := exec.Command(hsuPath)
cmd.Path = hsuPath
cmd.Stderr = os.Stderr // pass through fatal messages
cmd.Env = []string{"HAKUREI_APP_ID=" + strconv.Itoa(identity)}
cmd.Dir = container.FHSRoot
var (
p []byte
exitError *exec.ExitError
)
if p, u.err = cmd.Output(); u.err == nil {
u.uid, u.err = strconv.Atoi(string(p))
if u.err != nil {
u.err = hlog.WrapErr(u.err, "invalid uid string from hsu")
}
} else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
u.err = hlog.WrapErr(syscall.EACCES, "") // hsu prints to stderr in this case
} else if os.IsNotExist(u.err) {
u.err = hlog.WrapErr(os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", hsuPath))
}
return u.uid, u.err
}

8
internal/sysconf.go Normal file
View File

@ -0,0 +1,8 @@
package internal
//#include <unistd.h>
import "C"
const SC_LOGIN_NAME_MAX = C._SC_LOGIN_NAME_MAX
func Sysconf(name C.int) int { return int(C.sysconf(name)) }

View File

@ -35,7 +35,7 @@ package
*Default:* *Default:*
` <derivation hakurei-static-x86_64-unknown-linux-musl-0.2.2> ` ` <derivation hakurei-static-x86_64-unknown-linux-musl-0.2.0> `
@ -313,7 +313,7 @@ Extra paths to make available to the container\.
*Type:* *Type:*
list of attribute set of anything anything
@ -723,7 +723,7 @@ Common extra paths to make available to the container\.
*Type:* *Type:*
list of attribute set of anything anything
@ -759,7 +759,7 @@ package
*Default:* *Default:*
` <derivation hakurei-hsu-0.2.2> ` ` <derivation hakurei-hsu-0.2.0> `

View File

@ -203,7 +203,7 @@ in
}; };
extraPaths = mkOption { extraPaths = mkOption {
type = listOf (attrsOf anything); type = anything;
default = [ ]; default = [ ];
description = '' description = ''
Extra paths to make available to the container. Extra paths to make available to the container.
@ -261,7 +261,7 @@ in
}; };
commonPaths = mkOption { commonPaths = mkOption {
type = types.listOf (types.attrsOf types.anything); type = types.anything;
default = [ ]; default = [ ];
description = '' description = ''
Common extra paths to make available to the container. Common extra paths to make available to the container.

View File

@ -31,7 +31,7 @@
buildGoModule rec { buildGoModule rec {
pname = "hakurei"; pname = "hakurei";
version = "0.2.2"; version = "0.2.0";
srcFiltered = builtins.path { srcFiltered = builtins.path {
name = "${pname}-src"; name = "${pname}-src";

View File

@ -9,59 +9,65 @@ import (
"hakurei.app/system/acl" "hakurei.app/system/acl"
) )
// UpdatePerm calls UpdatePermType with the [Process] criteria. // UpdatePerm appends an ephemeral acl update Op.
func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I { func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I {
sys.UpdatePermType(Process, path, perms...) sys.UpdatePermType(Process, path, perms...)
return sys return sys
} }
// UpdatePermType maintains [acl.Perms] on a file until its [Enablement] is no longer satisfied. // UpdatePermType appends an acl update Op.
func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I { func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I {
sys.ops = append(sys.ops, &aclUpdateOp{et, path, perms}) sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &ACL{et, path, perms})
return sys return sys
} }
// aclUpdateOp implements [I.UpdatePermType]. type ACL struct {
type aclUpdateOp struct {
et Enablement et Enablement
path string path string
perms acl.Perms perms acl.Perms
} }
func (a *aclUpdateOp) Type() Enablement { return a.et } func (a *ACL) Type() Enablement { return a.et }
func (a *aclUpdateOp) apply(sys *I) error { func (a *ACL) apply(sys *I) error {
sys.verbose("applying ACL", a) msg.Verbose("applying ACL", a)
return newOpError("acl", sys.aclUpdate(a.path, sys.uid, a.perms...), false) return wrapErrSuffix(acl.Update(a.path, sys.uid, a.perms...),
fmt.Sprintf("cannot apply ACL entry to %q:", a.path))
} }
func (a *aclUpdateOp) revert(sys *I, ec *Criteria) error { func (a *ACL) revert(sys *I, ec *Criteria) error {
if ec.hasType(a.Type()) { if ec.hasType(a) {
sys.verbose("stripping ACL", a) msg.Verbose("stripping ACL", a)
err := sys.aclUpdate(a.path, sys.uid) err := acl.Update(a.path, sys.uid)
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
// the ACL is effectively stripped if the file no longer exists // the ACL is effectively stripped if the file no longer exists
sys.verbosef("target of ACL %s no longer exists", a) msg.Verbosef("target of ACL %s no longer exists", a)
err = nil err = nil
} }
return newOpError("acl", err, true) return wrapErrSuffix(err,
fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
} else { } else {
sys.verbose("skipping ACL", a) msg.Verbose("skipping ACL", a)
return nil return nil
} }
} }
func (a *aclUpdateOp) Is(o Op) bool { func (a *ACL) Is(o Op) bool {
target, ok := o.(*aclUpdateOp) a0, ok := o.(*ACL)
return ok && a != nil && target != nil && return ok && a0 != nil &&
a.et == target.et && a.et == a0.et &&
a.path == target.path && a.path == a0.path &&
slices.Equal(a.perms, target.perms) slices.Equal(a.perms, a0.perms)
} }
func (a *aclUpdateOp) Path() string { return a.path } func (a *ACL) Path() string { return a.path }
func (a *aclUpdateOp) String() string { func (a *ACL) String() string {
return fmt.Sprintf("%s type: %s path: %q", return fmt.Sprintf("%s type: %s path: %q",
a.perms, TypeString(a.et), a.path) a.perms, TypeString(a.et), a.path)
} }

View File

@ -29,5 +29,8 @@ func Update(name string, uid int, perms ...Perm) error {
(*C.acl_perm_t)(p), (*C.acl_perm_t)(p),
C.size_t(len(perms)), C.size_t(len(perms)),
) )
return newAclPathError(name, int(r), err) if r == 0 {
return nil
}
return err
} }

View File

@ -0,0 +1,156 @@
package acl_test
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os/exec"
"strconv"
)
type (
getFAclInvocation struct {
cmd *exec.Cmd
val []*getFAclResp
pe []error
}
getFAclResp struct {
typ fAclType
cred int32
val fAclPerm
raw []byte
}
fAclPerm uintptr
fAclType uint8
)
const fAclBufSize = 16
const (
fAclPermRead fAclPerm = 1 << iota
fAclPermWrite
fAclPermExecute
)
const (
fAclTypeUser fAclType = iota
fAclTypeGroup
fAclTypeMask
fAclTypeOther
)
func (c *getFAclInvocation) run(name string) error {
if c.cmd != nil {
panic("attempted to run twice")
}
c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name)
scanErr := make(chan error, 1)
if p, err := c.cmd.StdoutPipe(); err != nil {
return err
} else {
go c.parse(p, scanErr)
}
if err := c.cmd.Start(); err != nil {
return err
}
return errors.Join(<-scanErr, c.cmd.Wait())
}
func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
c.val = make([]*getFAclResp, 0, 4+fAclBufSize)
s := bufio.NewScanner(pipe)
for s.Scan() {
fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3)
if len(fields) != 3 {
continue
}
resp := getFAclResp{}
switch string(fields[0]) {
case "user":
resp.typ = fAclTypeUser
case "group":
resp.typ = fAclTypeGroup
case "mask":
resp.typ = fAclTypeMask
case "other":
resp.typ = fAclTypeOther
default:
c.pe = append(c.pe, fmt.Errorf("unknown type %s", string(fields[0])))
continue
}
if len(fields[1]) == 0 {
resp.cred = -1
} else {
if cred, err := strconv.Atoi(string(fields[1])); err != nil {
c.pe = append(c.pe, err)
continue
} else {
resp.cred = int32(cred)
if resp.cred < 0 {
c.pe = append(c.pe, fmt.Errorf("credential %d out of range", resp.cred))
continue
}
}
}
if len(fields[2]) != 3 {
c.pe = append(c.pe, fmt.Errorf("invalid perm length %d", len(fields[2])))
continue
} else {
switch fields[2][0] {
case 'r':
resp.val |= fAclPermRead
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0]))
continue
}
switch fields[2][1] {
case 'w':
resp.val |= fAclPermWrite
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1]))
continue
}
switch fields[2][2] {
case 'x':
resp.val |= fAclPermExecute
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2]))
continue
}
}
resp.raw = make([]byte, len(s.Bytes()))
copy(resp.raw, s.Bytes())
c.val = append(c.val, &resp)
}
scanErr <- s.Err()
}
func (r *getFAclResp) String() string {
if r.raw != nil && len(r.raw) > 0 {
return string(r.raw)
}
return "(user-initialised resp value)"
}
func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool {
return r.typ == typ && r.cred == cred && r.val == val
}

View File

@ -1,16 +1,10 @@
package acl_test package acl_test
import ( import (
"bufio"
"bytes"
"errors" "errors"
"fmt"
"io"
"os" "os"
"os/exec"
"path" "path"
"reflect" "reflect"
"strconv"
"testing" "testing"
"hakurei.app/system/acl" "hakurei.app/system/acl"
@ -23,7 +17,7 @@ var (
cred = int32(os.Geteuid()) cred = int32(os.Geteuid())
) )
func TestUpdate(t *testing.T) { func TestUpdatePerm(t *testing.T) {
if os.Getenv("GO_TEST_SKIP_ACL") == "1" { if os.Getenv("GO_TEST_SKIP_ACL") == "1" {
t.Log("acl test skipped") t.Log("acl test skipped")
t.SkipNow() t.SkipNow()
@ -54,19 +48,19 @@ func TestUpdate(t *testing.T) {
t.Run("default clear mask", func(t *testing.T) { t.Run("default clear mask", func(t *testing.T) {
if err := acl.Update(testFilePath, uid); err != nil { if err := acl.Update(testFilePath, uid); err != nil {
t.Fatalf("Update: error = %v", err) t.Fatalf("UpdatePerm: error = %v", err)
} }
if cur = getfacl(t, testFilePath); len(cur) != 4 { if cur = getfacl(t, testFilePath); len(cur) != 4 {
t.Fatalf("Update: %v", cur) t.Fatalf("UpdatePerm: %v", cur)
} }
}) })
t.Run("default clear consistency", func(t *testing.T) { t.Run("default clear consistency", func(t *testing.T) {
if err := acl.Update(testFilePath, uid); err != nil { if err := acl.Update(testFilePath, uid); err != nil {
t.Fatalf("Update: error = %v", err) t.Fatalf("UpdatePerm: error = %v", err)
} }
if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) { if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) {
t.Fatalf("Update: %v, want %v", val, cur) t.Fatalf("UpdatePerm: %v, want %v", val, cur)
} }
}) })
@ -83,171 +77,26 @@ func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Cleanup(func() { t.Cleanup(func() {
if err := acl.Update(testFilePath, uid); err != nil { if err := acl.Update(testFilePath, uid); err != nil {
t.Fatalf("Update: error = %v", err) t.Fatalf("UpdatePerm: error = %v", err)
} }
if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) { if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) {
t.Fatalf("Update: %v, want %v", v, cur) t.Fatalf("UpdatePerm: %v, want %v", v, cur)
} }
}) })
if err := acl.Update(testFilePath, uid, perms...); err != nil { if err := acl.Update(testFilePath, uid, perms...); err != nil {
t.Fatalf("Update: error = %v", err) t.Fatalf("UpdatePerm: error = %v", err)
} }
r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred) r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred)
if r == nil { if r == nil {
t.Fatalf("Update did not add an ACL entry") t.Fatalf("UpdatePerm did not add an ACL entry")
} }
if !r.equals(fAclTypeUser, cred, val) { if !r.equals(fAclTypeUser, cred, val) {
t.Fatalf("Update(%s) = %s", name, r) t.Fatalf("UpdatePerm(%s) = %s", name, r)
} }
}) })
} }
type (
getFAclInvocation struct {
cmd *exec.Cmd
val []*getFAclResp
pe []error
}
getFAclResp struct {
typ fAclType
cred int32
val fAclPerm
raw []byte
}
fAclPerm uintptr
fAclType uint8
)
const fAclBufSize = 16
const (
fAclPermRead fAclPerm = 1 << iota
fAclPermWrite
fAclPermExecute
)
const (
fAclTypeUser fAclType = iota
fAclTypeGroup
fAclTypeMask
fAclTypeOther
)
func (c *getFAclInvocation) run(name string) error {
if c.cmd != nil {
panic("attempted to run twice")
}
c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name)
scanErr := make(chan error, 1)
if p, err := c.cmd.StdoutPipe(); err != nil {
return err
} else {
go c.parse(p, scanErr)
}
if err := c.cmd.Start(); err != nil {
return err
}
return errors.Join(<-scanErr, c.cmd.Wait())
}
func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
c.val = make([]*getFAclResp, 0, 4+fAclBufSize)
s := bufio.NewScanner(pipe)
for s.Scan() {
fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3)
if len(fields) != 3 {
continue
}
resp := getFAclResp{}
switch string(fields[0]) {
case "user":
resp.typ = fAclTypeUser
case "group":
resp.typ = fAclTypeGroup
case "mask":
resp.typ = fAclTypeMask
case "other":
resp.typ = fAclTypeOther
default:
c.pe = append(c.pe, fmt.Errorf("unknown type %s", string(fields[0])))
continue
}
if len(fields[1]) == 0 {
resp.cred = -1
} else {
if cred, err := strconv.Atoi(string(fields[1])); err != nil {
c.pe = append(c.pe, err)
continue
} else {
resp.cred = int32(cred)
if resp.cred < 0 {
c.pe = append(c.pe, fmt.Errorf("credential %d out of range", resp.cred))
continue
}
}
}
if len(fields[2]) != 3 {
c.pe = append(c.pe, fmt.Errorf("invalid perm length %d", len(fields[2])))
continue
} else {
switch fields[2][0] {
case 'r':
resp.val |= fAclPermRead
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0]))
continue
}
switch fields[2][1] {
case 'w':
resp.val |= fAclPermWrite
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1]))
continue
}
switch fields[2][2] {
case 'x':
resp.val |= fAclPermExecute
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2]))
continue
}
}
resp.raw = make([]byte, len(s.Bytes()))
copy(resp.raw, s.Bytes())
c.val = append(c.val, &resp)
}
scanErr <- s.Err()
}
func (r *getFAclResp) String() string {
if r.raw != nil && len(r.raw) > 0 {
return string(r.raw)
}
return "(user-initialised resp value)"
}
func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool {
return r.typ == typ && r.cred == cred && r.val == val
}
func getfacl(t *testing.T, name string) []*getFAclResp { func getfacl(t *testing.T, name string) []*getFAclResp {
c := new(getFAclInvocation) c := new(getFAclInvocation)
if err := c.run(name); err != nil { if err := c.run(name); err != nil {

View File

@ -6,7 +6,7 @@
int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid, int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
acl_perm_t *perms, size_t plen) { acl_perm_t *perms, size_t plen) {
int ret; int ret = -1;
bool v; bool v;
int i; int i;
acl_t acl; acl_t acl;
@ -15,70 +15,51 @@ int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
void *qualifier_p; void *qualifier_p;
acl_permset_t permset; acl_permset_t permset;
ret = -1; /* acl_get_file */
acl = acl_get_file(path_p, ACL_TYPE_ACCESS); acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
if (acl == NULL) if (acl == NULL)
goto out; goto out;
/* prune entries by uid */ // prune entries by uid
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1; for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) { i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
ret = -2; /* acl_get_tag_type */
if (acl_get_tag_type(entry, &tag_type) != 0) if (acl_get_tag_type(entry, &tag_type) != 0)
goto out; return -1;
if (tag_type != ACL_USER) if (tag_type != ACL_USER)
continue; continue;
ret = -3; /* acl_get_qualifier */
qualifier_p = acl_get_qualifier(entry); qualifier_p = acl_get_qualifier(entry);
if (qualifier_p == NULL) if (qualifier_p == NULL)
goto out; return -1;
v = *(uid_t *)qualifier_p == uid; v = *(uid_t *)qualifier_p == uid;
acl_free(qualifier_p); acl_free(qualifier_p);
if (!v) if (!v)
continue; continue;
ret = -4; /* acl_delete_entry */ acl_delete_entry(acl, entry);
if (acl_delete_entry(acl, entry) != 0)
goto out;
} }
if (plen == 0) if (plen == 0)
goto set; goto set;
ret = -5; /* acl_create_entry */
if (acl_create_entry(&acl, &entry) != 0) if (acl_create_entry(&acl, &entry) != 0)
goto out; goto out;
ret = -6; /* acl_get_permset */
if (acl_get_permset(entry, &permset) != 0) if (acl_get_permset(entry, &permset) != 0)
goto out; goto out;
ret = -7; /* acl_add_perm */
for (i = 0; i < plen; i++) { for (i = 0; i < plen; i++) {
if (acl_add_perm(permset, perms[i]) != 0) if (acl_add_perm(permset, perms[i]) != 0)
goto out; goto out;
} }
ret = -8; /* acl_set_tag_type */
if (acl_set_tag_type(entry, ACL_USER) != 0) if (acl_set_tag_type(entry, ACL_USER) != 0)
goto out; goto out;
ret = -9; /* acl_set_qualifier */
if (acl_set_qualifier(entry, (void *)&uid) != 0) if (acl_set_qualifier(entry, (void *)&uid) != 0)
goto out; goto out;
set: set:
ret = -10; /* acl_calc_mask */
if (acl_calc_mask(&acl) != 0) if (acl_calc_mask(&acl) != 0)
goto out; goto out;
ret = -11; /* acl_valid */
if (acl_valid(acl) != 0) if (acl_valid(acl) != 0)
goto out; goto out;
ret = -12; /* acl_set_file */
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0) if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
ret = 0; ret = 0;

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