Compare commits
38 Commits
ecaf43358d
..
v0.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
ad1bc6794f
|
|||
|
e55822c62f
|
|||
|
802e6afa34
|
|||
|
e906cae9ee
|
|||
|
ae2df2c450
|
|||
|
6e3f34f2ec
|
|||
|
65a0bb9729
|
|||
|
afa7a0800d
|
|||
|
773253fdf5
|
|||
|
409ed172c8
|
|||
|
1c4f593566
|
|||
|
b99c63337d
|
|||
|
f09133a224
|
|||
|
16409b37a2
|
|||
|
a2a291791c
|
|||
|
8690419c2d
|
|||
|
1cdc6b4246
|
|||
|
56aad8dc11
|
|||
|
83c4f8b767
|
|||
|
d0ddd71934
|
|||
|
70e02090f7
|
|||
|
ca247b8037
|
|||
|
3f25c3f0af
|
|||
|
e271fa77aa
|
|||
|
f876043844
|
|||
|
6265aea73a
|
|||
|
c8a0effe90
|
|||
|
8df01b71d4
|
|||
|
985c4dd2fc
|
|||
|
da2b9c01ce
|
|||
|
323d132c40
|
|||
|
6cc2b406a4
|
|||
|
fcd0f2ede7
|
|||
|
e68db7fbfc
|
|||
|
ac81cfbedc
|
|||
|
05db06c87b
|
|||
|
e603b688ca
|
|||
|
a9def08533
|
+79
-100
@@ -6,11 +6,9 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/command"
|
||||
@@ -24,7 +22,7 @@ import (
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func buildCommand(out io.Writer) command.Command {
|
||||
func buildCommand(ctx context.Context, out io.Writer) command.Command {
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagJSON bool
|
||||
@@ -44,35 +42,35 @@ func buildCommand(out io.Writer) command.Command {
|
||||
config := tryPath(args[0])
|
||||
config.Args = append(config.Args, args[1:]...)
|
||||
|
||||
runApp(config)
|
||||
app.Main(ctx, config)
|
||||
panic("unreachable")
|
||||
})
|
||||
|
||||
{
|
||||
var (
|
||||
dbusConfigSession string
|
||||
dbusConfigSystem string
|
||||
mpris bool
|
||||
dbusVerbose bool
|
||||
flagDBusConfigSession string
|
||||
flagDBusConfigSystem string
|
||||
flagDBusMpris bool
|
||||
flagDBusVerbose bool
|
||||
|
||||
fid string
|
||||
aid int
|
||||
groups command.RepeatableFlag
|
||||
homeDir string
|
||||
userName string
|
||||
flagID string
|
||||
flagIdentity int
|
||||
flagGroups command.RepeatableFlag
|
||||
flagHomeDir string
|
||||
flagUserName string
|
||||
|
||||
wayland, x11, dBus, pulse bool
|
||||
flagWayland, flagX11, flagDBus, flagPulse bool
|
||||
)
|
||||
|
||||
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
|
||||
// initialise config from flags
|
||||
config := &hst.Config{
|
||||
ID: fid,
|
||||
ID: flagID,
|
||||
Args: args,
|
||||
}
|
||||
|
||||
if aid < 0 || aid > 9999 {
|
||||
log.Fatalf("aid %d out of range", aid)
|
||||
if flagIdentity < 0 || flagIdentity > 9999 {
|
||||
log.Fatalf("identity %d out of range", flagIdentity)
|
||||
}
|
||||
|
||||
// resolve home/username from os when flag is unset
|
||||
@@ -80,14 +78,7 @@ func buildCommand(out io.Writer) command.Command {
|
||||
passwd *user.User
|
||||
passwdOnce sync.Once
|
||||
passwdFunc = func() {
|
||||
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)
|
||||
}
|
||||
|
||||
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustID(), flagIdentity))
|
||||
if u, err := user.LookupId(us); err != nil {
|
||||
hlog.Verbosef("cannot look up uid %s", us)
|
||||
passwd = &user.User{
|
||||
@@ -103,21 +94,21 @@ func buildCommand(out io.Writer) command.Command {
|
||||
}
|
||||
)
|
||||
|
||||
if homeDir == "os" {
|
||||
if flagHomeDir == "os" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
homeDir = passwd.HomeDir
|
||||
flagHomeDir = passwd.HomeDir
|
||||
}
|
||||
|
||||
if userName == "chronos" {
|
||||
if flagUserName == "chronos" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
userName = passwd.Username
|
||||
flagUserName = passwd.Username
|
||||
}
|
||||
|
||||
config.Identity = aid
|
||||
config.Groups = groups
|
||||
config.Username = userName
|
||||
config.Identity = flagIdentity
|
||||
config.Groups = flagGroups
|
||||
config.Username = flagUserName
|
||||
|
||||
if a, err := container.NewAbs(homeDir); err != nil {
|
||||
if a, err := container.NewAbs(flagHomeDir); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return err
|
||||
} else {
|
||||
@@ -125,43 +116,43 @@ func buildCommand(out io.Writer) command.Command {
|
||||
}
|
||||
|
||||
var e system.Enablement
|
||||
if wayland {
|
||||
if flagWayland {
|
||||
e |= system.EWayland
|
||||
}
|
||||
if x11 {
|
||||
if flagX11 {
|
||||
e |= system.EX11
|
||||
}
|
||||
if dBus {
|
||||
if flagDBus {
|
||||
e |= system.EDBus
|
||||
}
|
||||
if pulse {
|
||||
if flagPulse {
|
||||
e |= system.EPulse
|
||||
}
|
||||
config.Enablements = hst.NewEnablements(e)
|
||||
|
||||
// parse D-Bus config file from flags if applicable
|
||||
if dBus {
|
||||
if dbusConfigSession == "builtin" {
|
||||
config.SessionBus = dbus.NewConfig(fid, true, mpris)
|
||||
if flagDBus {
|
||||
if flagDBusConfigSession == "builtin" {
|
||||
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
|
||||
} else {
|
||||
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
|
||||
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
|
||||
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSession); err != nil {
|
||||
log.Fatalf("cannot load session bus proxy config from %q: %s", flagDBusConfigSession, err)
|
||||
} else {
|
||||
config.SessionBus = conf
|
||||
}
|
||||
}
|
||||
|
||||
// system bus proxy is optional
|
||||
if dbusConfigSystem != "nil" {
|
||||
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
|
||||
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
|
||||
if flagDBusConfigSystem != "nil" {
|
||||
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSystem); err != nil {
|
||||
log.Fatalf("cannot load system bus proxy config from %q: %s", flagDBusConfigSystem, err)
|
||||
} else {
|
||||
config.SystemBus = conf
|
||||
}
|
||||
}
|
||||
|
||||
// override log from configuration
|
||||
if dbusVerbose {
|
||||
if flagDBusVerbose {
|
||||
if config.SessionBus != nil {
|
||||
config.SessionBus.Log = true
|
||||
}
|
||||
@@ -171,63 +162,68 @@ func buildCommand(out io.Writer) command.Command {
|
||||
}
|
||||
}
|
||||
|
||||
// invoke app
|
||||
runApp(config)
|
||||
app.Main(ctx, config)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||
"Path to session bus proxy config file, or \"builtin\" for defaults").
|
||||
Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"),
|
||||
Flag(&flagDBusConfigSystem, "dbus-system", command.StringFlag("nil"),
|
||||
"Path to system bus proxy config file, or \"nil\" to disable").
|
||||
Flag(&mpris, "mpris", command.BoolFlag(false),
|
||||
Flag(&flagDBusMpris, "mpris", command.BoolFlag(false),
|
||||
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
|
||||
Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false),
|
||||
Flag(&flagDBusVerbose, "dbus-log", command.BoolFlag(false),
|
||||
"Force buffered logging in the D-Bus proxy").
|
||||
Flag(&fid, "id", command.StringFlag(""),
|
||||
Flag(&flagID, "id", command.StringFlag(""),
|
||||
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
|
||||
Flag(&aid, "a", command.IntFlag(0),
|
||||
Flag(&flagIdentity, "a", command.IntFlag(0),
|
||||
"Application identity").
|
||||
Flag(nil, "g", &groups,
|
||||
Flag(nil, "g", &flagGroups,
|
||||
"Groups inherited by all container processes").
|
||||
Flag(&homeDir, "d", command.StringFlag("os"),
|
||||
Flag(&flagHomeDir, "d", command.StringFlag("os"),
|
||||
"Container home directory").
|
||||
Flag(&userName, "u", command.StringFlag("chronos"),
|
||||
Flag(&flagUserName, "u", command.StringFlag("chronos"),
|
||||
"Passwd user name within sandbox").
|
||||
Flag(&wayland, "wayland", command.BoolFlag(false),
|
||||
Flag(&flagWayland, "wayland", command.BoolFlag(false),
|
||||
"Enable connection to Wayland via security-context-v1").
|
||||
Flag(&x11, "X", command.BoolFlag(false),
|
||||
Flag(&flagX11, "X", command.BoolFlag(false),
|
||||
"Enable direct connection to X11").
|
||||
Flag(&dBus, "dbus", command.BoolFlag(false),
|
||||
Flag(&flagDBus, "dbus", command.BoolFlag(false),
|
||||
"Enable proxied connection to D-Bus").
|
||||
Flag(&pulse, "pulse", command.BoolFlag(false),
|
||||
Flag(&flagPulse, "pulse", command.BoolFlag(false),
|
||||
"Enable direct connection to PulseAudio")
|
||||
}
|
||||
|
||||
var showFlagShort bool
|
||||
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
|
||||
switch len(args) {
|
||||
case 0: // system
|
||||
printShowSystem(os.Stdout, showFlagShort, flagJSON)
|
||||
{
|
||||
var flagShort bool
|
||||
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
|
||||
switch len(args) {
|
||||
case 0: // system
|
||||
printShowSystem(os.Stdout, flagShort, flagJSON)
|
||||
|
||||
case 1: // instance
|
||||
name := args[0]
|
||||
config, entry := tryShort(name)
|
||||
if config == nil {
|
||||
config = tryPath(name)
|
||||
case 1: // instance
|
||||
name := args[0]
|
||||
config, entry := tryShort(name)
|
||||
if config == nil {
|
||||
config = tryPath(name)
|
||||
}
|
||||
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON)
|
||||
|
||||
default:
|
||||
log.Fatal("show requires 1 argument")
|
||||
}
|
||||
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
|
||||
return errSuccess
|
||||
}).Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information")
|
||||
}
|
||||
|
||||
default:
|
||||
log.Fatal("show requires 1 argument")
|
||||
}
|
||||
return errSuccess
|
||||
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
|
||||
|
||||
var psFlagShort bool
|
||||
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")
|
||||
{
|
||||
var flagShort bool
|
||||
c.NewCommand("ps", "List active instances", func(args []string) error {
|
||||
var sc hst.Paths
|
||||
app.CopyPaths(&sc, new(app.Hsu).MustID())
|
||||
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sc.RunDirPath.String()), flagShort, flagJSON)
|
||||
return errSuccess
|
||||
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||
}
|
||||
|
||||
c.Command("version", "Display version information", func(args []string) error {
|
||||
fmt.Println(internal.Version())
|
||||
@@ -251,20 +247,3 @@ func buildCommand(out io.Writer) command.Command {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ Flags:
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out := new(bytes.Buffer)
|
||||
c := buildCommand(out)
|
||||
c := buildCommand(t.Context(), out)
|
||||
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
|
||||
t.Errorf("Parse: error = %v; want %v",
|
||||
err, command.ErrHelp)
|
||||
|
||||
+8
-4
@@ -4,15 +4,17 @@ package main
|
||||
//go:generate cp ../../LICENSE .
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/sys"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,8 +26,6 @@ var (
|
||||
|
||||
func init() { hlog.Prepare("hakurei") }
|
||||
|
||||
var std sys.State = new(sys.Std)
|
||||
|
||||
func main() {
|
||||
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
||||
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
|
||||
@@ -44,7 +44,11 @@ func main() {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
|
||||
buildCommand(ctx, os.Stderr).MustParse(os.Args[1:], func(err error) {
|
||||
hlog.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
hlog.BeforeExit()
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/app"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
@@ -87,7 +88,9 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
|
||||
if likePrefix && len(name) >= 8 {
|
||||
hlog.Verbose("argument looks like prefix")
|
||||
|
||||
s := state.NewMulti(std.Paths().RunDirPath.String())
|
||||
var sc hst.Paths
|
||||
app.CopyPaths(&sc, new(app.Hsu).MustID())
|
||||
s := state.NewMulti(sc.RunDirPath.String())
|
||||
if entries, err := state.Join(s); err != nil {
|
||||
log.Printf("cannot join store: %v", err)
|
||||
// drop to fetch from file
|
||||
|
||||
+3
-11
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -13,8 +12,8 @@ import (
|
||||
"time"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/app"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
@@ -22,15 +21,8 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
info := &hst.Info{Paths: std.Paths()}
|
||||
|
||||
// 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
|
||||
}
|
||||
info := &hst.Info{User: new(app.Hsu).MustID()}
|
||||
app.CopyPaths(&info.Paths, info.User)
|
||||
|
||||
if flagJSON {
|
||||
printJSON(output, short, info)
|
||||
|
||||
+21
-16
@@ -15,7 +15,7 @@ import (
|
||||
const (
|
||||
hsuConfFile = "/etc/hsurc"
|
||||
envShim = "HAKUREI_SHIM"
|
||||
envAID = "HAKUREI_APP_ID"
|
||||
envIdentity = "HAKUREI_IDENTITY"
|
||||
envGroups = "HAKUREI_GROUPS"
|
||||
|
||||
PR_SET_NO_NEW_PRIVS = 0x26
|
||||
@@ -48,8 +48,8 @@ func main() {
|
||||
}
|
||||
|
||||
// uid = 1000000 +
|
||||
// fid * 10000 +
|
||||
// aid
|
||||
// id * 10000 +
|
||||
// identity
|
||||
uid := 1000000
|
||||
|
||||
// refuse to run if hsurc is not protected correctly
|
||||
@@ -62,29 +62,25 @@ func main() {
|
||||
}
|
||||
|
||||
// authenticate before accepting user input
|
||||
var id int
|
||||
if f, err := os.Open(hsuConfFile); err != nil {
|
||||
log.Fatal(err)
|
||||
} else if fid, ok := mustParseConfig(f, puid); !ok {
|
||||
} else if v, ok := mustParseConfig(f, puid); !ok {
|
||||
log.Fatalf("uid %d is not in the hsurc file", puid)
|
||||
} else {
|
||||
uid += fid * 10000
|
||||
}
|
||||
id = v
|
||||
if err = f.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 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
|
||||
uid += id * 10000
|
||||
}
|
||||
|
||||
// pass through setup fd to shim
|
||||
var shimSetupFd string
|
||||
if s, ok := os.LookupEnv(envShim); !ok {
|
||||
// hakurei requests target uid
|
||||
// print resolved uid and exit
|
||||
fmt.Print(uid)
|
||||
// hakurei requests hsurc user id
|
||||
fmt.Print(id)
|
||||
os.Exit(0)
|
||||
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
|
||||
log.Fatal("HAKUREI_SHIM holds an invalid value")
|
||||
@@ -92,6 +88,15 @@ func main() {
|
||||
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
|
||||
var suppGroups, suppCurrent []int
|
||||
|
||||
|
||||
@@ -64,5 +64,5 @@ func (e *AutoEtcOp) Is(op Op) bool {
|
||||
ve, ok := op.(*AutoEtcOp)
|
||||
return ok && e.Valid() && ve.Valid() && *e == *ve
|
||||
}
|
||||
func (*AutoEtcOp) prefix() string { return "setting up" }
|
||||
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
||||
func (*AutoEtcOp) prefix() (string, bool) { return "setting up", true }
|
||||
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
||||
|
||||
@@ -22,7 +22,7 @@ type AutoRootOp struct {
|
||||
// obtained during early;
|
||||
// these wrap the underlying Op because BindMountOp is relatively complex,
|
||||
// so duplicating that code would be unwise
|
||||
resolved []Op
|
||||
resolved []*BindMountOp
|
||||
}
|
||||
|
||||
func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil }
|
||||
@@ -31,10 +31,11 @@ func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
|
||||
if d, err := k.readdir(r.Host.String()); err != nil {
|
||||
return err
|
||||
} else {
|
||||
r.resolved = make([]Op, 0, len(d))
|
||||
r.resolved = make([]*BindMountOp, 0, len(d))
|
||||
for _, ent := range d {
|
||||
name := ent.Name()
|
||||
if IsAutoRootBindable(name) {
|
||||
// careful: the Valid method is skipped, make sure this is always valid
|
||||
op := &BindMountOp{
|
||||
Source: r.Host.Append(name),
|
||||
Target: AbsFHSRoot.Append(name),
|
||||
@@ -57,7 +58,7 @@ func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
state.nonrepeatable |= nrAutoRoot
|
||||
|
||||
for _, op := range r.resolved {
|
||||
k.verbosef("%s %s", op.prefix(), op)
|
||||
// these are exclusively BindMountOp, do not attempt to print identifying message
|
||||
if err := op.apply(state, k); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -71,7 +72,7 @@ func (r *AutoRootOp) Is(op Op) bool {
|
||||
r.Host.Is(vr.Host) &&
|
||||
r.Flags == vr.Flags
|
||||
}
|
||||
func (*AutoRootOp) prefix() string { return "setting up" }
|
||||
func (*AutoRootOp) prefix() (string, bool) { return "setting up", true }
|
||||
func (r *AutoRootOp) String() string {
|
||||
return fmt.Sprintf("auto root %q flags %#x", r.Host, r.Flags)
|
||||
}
|
||||
|
||||
+23
-24
@@ -51,7 +51,6 @@ func TestAutoRootOp(t *testing.T) {
|
||||
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(false), stub.UniqueError(0)),
|
||||
}, stub.UniqueError(0)},
|
||||
|
||||
@@ -73,17 +72,17 @@ func TestAutoRootOp(t *testing.T) {
|
||||
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/home"), MustAbs("/home"), MustAbs("/home"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lib64"), MustAbs("/lib64"), MustAbs("/lib64"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lost+found"), MustAbs("/lost+found"), MustAbs("/lost+found"), BindWritable}}}, 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("bindMount", stub.ExpectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/nix"), MustAbs("/nix"), MustAbs("/nix"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/root"), MustAbs("/root"), MustAbs("/root"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/run"), MustAbs("/run"), MustAbs("/run"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/srv"), MustAbs("/srv"), MustAbs("/srv"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/sys"), MustAbs("/sys"), MustAbs("/sys"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr"), MustAbs("/usr"), MustAbs("/usr"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var"), MustAbs("/var"), MustAbs("/var"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
}, nil},
|
||||
|
||||
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||
@@ -103,17 +102,17 @@ func TestAutoRootOp(t *testing.T) {
|
||||
call("evalSymlinks", stub.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),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
||||
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil),
|
||||
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil),
|
||||
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil),
|
||||
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil),
|
||||
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil),
|
||||
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil),
|
||||
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil),
|
||||
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil),
|
||||
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil),
|
||||
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
}, nil},
|
||||
})
|
||||
|
||||
@@ -141,7 +140,7 @@ func TestAutoRootOp(t *testing.T) {
|
||||
}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
resolved: []Op{new(BindMountOp)},
|
||||
resolved: []*BindMountOp{new(BindMountOp)},
|
||||
}, true},
|
||||
|
||||
{"flags differs", &AutoRootOp{
|
||||
|
||||
+52
-12
@@ -64,7 +64,7 @@ type (
|
||||
Args []string
|
||||
// Deliver SIGINT to the initial process on context cancellation.
|
||||
ForwardCancel bool
|
||||
// time to wait for linger processes after death of initial process
|
||||
// Time to wait for processes lingering after the initial process terminates.
|
||||
AdoptWaitDelay time.Duration
|
||||
|
||||
// Mapped Uid in user namespace.
|
||||
@@ -152,15 +152,13 @@ func (e *StartError) Message() string {
|
||||
|
||||
// Start starts the container init. The init process blocks until Serve is called.
|
||||
func (p *Container) Start() error {
|
||||
if p.cmd != nil {
|
||||
if p == nil || 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")
|
||||
}
|
||||
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
|
||||
if p.Uid < 1 {
|
||||
@@ -182,9 +180,17 @@ func (p *Container) Start() error {
|
||||
p.AdoptWaitDelay = 0
|
||||
}
|
||||
|
||||
p.cmd = exec.CommandContext(ctx, MustExecutable())
|
||||
if p.cmd.Stdin == nil {
|
||||
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.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
|
||||
p.cmd.WaitDelay = p.WaitDelay
|
||||
if p.Cancel != nil {
|
||||
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
|
||||
@@ -330,7 +336,7 @@ func (p *Container) Serve() error {
|
||||
|
||||
// Wait waits for the container init process to exit and releases any resources associated with the [Container].
|
||||
func (p *Container) Wait() error {
|
||||
if p.cmd == nil {
|
||||
if p.cmd == nil || p.cmd.Process == nil {
|
||||
return EINVAL
|
||||
}
|
||||
|
||||
@@ -342,6 +348,36 @@ func (p *Container) Wait() error {
|
||||
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 {
|
||||
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))
|
||||
@@ -357,7 +393,11 @@ func (p *Container) ProcessState() *os.ProcessState {
|
||||
|
||||
// New returns the address to a new instance of [Container] that requires further initialisation before use.
|
||||
func New(ctx context.Context) *Container {
|
||||
return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
|
||||
p := &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.
|
||||
|
||||
@@ -53,7 +53,7 @@ type syscallDispatcher interface {
|
||||
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
||||
|
||||
// bindMount provides procPaths.bindMount.
|
||||
bindMount(source, target string, flags uintptr, eq bool) error
|
||||
bindMount(source, target string, flags uintptr) error
|
||||
// remount provides procPaths.remount.
|
||||
remount(target string, flags uintptr) error
|
||||
// mountTmpfs provides mountTmpfs.
|
||||
@@ -161,8 +161,8 @@ func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
return Receive(key, e, fdp)
|
||||
}
|
||||
|
||||
func (direct) bindMount(source, target string, flags uintptr, eq bool) error {
|
||||
return hostProc.bindMount(source, target, flags, eq)
|
||||
func (direct) bindMount(source, target string, flags uintptr) error {
|
||||
return hostProc.bindMount(source, target, flags)
|
||||
}
|
||||
func (direct) remount(target string, flags uintptr) error {
|
||||
return hostProc.remount(target, flags)
|
||||
|
||||
@@ -111,7 +111,7 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
@@ -403,13 +403,12 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
|
||||
return
|
||||
}
|
||||
|
||||
func (k *kstub) bindMount(source, target string, flags uintptr, eq bool) error {
|
||||
func (k *kstub) bindMount(source, target string, flags uintptr) error {
|
||||
k.Helper()
|
||||
return k.Expects("bindMount").Error(
|
||||
stub.CheckArg(k.Stub, "source", source, 0),
|
||||
stub.CheckArg(k.Stub, "target", target, 1),
|
||||
stub.CheckArg(k.Stub, "flags", flags, 2),
|
||||
stub.CheckArg(k.Stub, "eq", eq, 3))
|
||||
stub.CheckArg(k.Stub, "flags", flags, 2))
|
||||
}
|
||||
|
||||
func (k *kstub) remount(target string, flags uintptr) error {
|
||||
|
||||
+6
-2
@@ -47,7 +47,9 @@ type (
|
||||
// apply is called in intermediate root.
|
||||
apply(state *setupState, k syscallDispatcher) error
|
||||
|
||||
prefix() string
|
||||
// prefix returns a log message prefix, and whether this Op prints no identifying message on its own.
|
||||
prefix() (string, bool)
|
||||
|
||||
Is(op Op) bool
|
||||
Valid() bool
|
||||
fmt.Stringer
|
||||
@@ -223,7 +225,9 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
|
||||
chdir is allowed but discouraged */
|
||||
for i, op := range *params.Ops {
|
||||
// ops already checked during early setup
|
||||
k.verbosef("%s %s", op.prefix(), op)
|
||||
if prefix, ok := op.prefix(); ok {
|
||||
k.verbosef("%s %s", prefix, op)
|
||||
}
|
||||
if err := op.apply(state, k); err != nil {
|
||||
if m, ok := messageFromError(err); ok {
|
||||
k.fatal(m)
|
||||
|
||||
+23
-23
@@ -790,9 +790,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -844,9 +844,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -898,9 +898,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -953,9 +953,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1009,9 +1009,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1067,9 +1067,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1126,9 +1126,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1186,9 +1186,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1247,9 +1247,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1309,9 +1309,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1372,9 +1372,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1436,9 +1436,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1501,9 +1501,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1574,9 +1574,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1680,9 +1680,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1787,9 +1787,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
@@ -1895,9 +1895,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0750)}, nil, nil),
|
||||
@@ -2008,9 +2008,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0750)}, nil, nil),
|
||||
@@ -2107,9 +2107,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0750)}, nil, nil),
|
||||
@@ -2197,9 +2197,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0750)}, nil, nil),
|
||||
@@ -2289,9 +2289,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0750)}, nil, nil),
|
||||
@@ -2388,9 +2388,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0750)}, nil, nil),
|
||||
@@ -2524,9 +2524,9 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
|
||||
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
|
||||
/* begin apply */
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{sourceFinal: MustAbs("/"), Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindDevice}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: MustAbs("/proc/")}}}, nil, nil),
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, nil),
|
||||
|
||||
@@ -91,7 +91,12 @@ func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||
flags |= syscall.MS_NODEV
|
||||
}
|
||||
|
||||
return k.bindMount(source, target, flags, b.sourceFinal == b.Target)
|
||||
if b.sourceFinal.String() == b.Target.String() {
|
||||
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 {
|
||||
@@ -101,7 +106,7 @@ func (b *BindMountOp) Is(op Op) bool {
|
||||
b.Target.Is(vb.Target) &&
|
||||
b.Flags == vb.Flags
|
||||
}
|
||||
func (*BindMountOp) prefix() string { return "mounting" }
|
||||
func (*BindMountOp) prefix() (string, bool) { return "mounting", false }
|
||||
func (b *BindMountOp) String() string {
|
||||
if b.Source == nil || b.Target == nil {
|
||||
return "<invalid>"
|
||||
|
||||
@@ -35,6 +35,7 @@ func TestBindMountOp(t *testing.T) {
|
||||
}, nil, []stub.Call{
|
||||
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(0x4005)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
@@ -67,6 +68,7 @@ func TestBindMountOp(t *testing.T) {
|
||||
}, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||
call("mkdirAll", stub.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),
|
||||
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
@@ -79,6 +81,7 @@ func TestBindMountOp(t *testing.T) {
|
||||
}, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
|
||||
call("ensureFile", stub.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),
|
||||
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
@@ -91,6 +94,7 @@ func TestBindMountOp(t *testing.T) {
|
||||
}, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
|
||||
call("ensureFile", stub.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),
|
||||
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
@@ -128,9 +132,22 @@ func TestBindMountOp(t *testing.T) {
|
||||
}, nil, []stub.Call{
|
||||
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(0x4005)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, stub.UniqueError(0)),
|
||||
}, 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{
|
||||
Source: MustAbs("/bin/"),
|
||||
Target: MustAbs("/bin/"),
|
||||
@@ -139,6 +156,7 @@ func TestBindMountOp(t *testing.T) {
|
||||
}, nil, []stub.Call{
|
||||
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(0x4005)}}, nil, nil),
|
||||
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
||||
}, nil},
|
||||
})
|
||||
|
||||
@@ -49,7 +49,6 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
toHost(FHSDev+name),
|
||||
targetPath,
|
||||
0,
|
||||
true,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -97,7 +96,6 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
toHost(name),
|
||||
consolePath,
|
||||
0,
|
||||
false,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -131,7 +129,7 @@ func (d *MountDevOp) Is(op Op) bool {
|
||||
d.Mqueue == vd.Mqueue &&
|
||||
d.Write == vd.Write
|
||||
}
|
||||
func (*MountDevOp) prefix() string { return "mounting" }
|
||||
func (*MountDevOp) prefix() (string, bool) { return "mounting", true }
|
||||
func (d *MountDevOp) String() string {
|
||||
if d.Mqueue {
|
||||
return fmt.Sprintf("dev on %q with mqueue", d.Target)
|
||||
|
||||
@@ -32,5 +32,5 @@ func (m *MkdirOp) Is(op Op) bool {
|
||||
m.Path.Is(vm.Path) &&
|
||||
m.Perm == vm.Perm
|
||||
}
|
||||
func (*MkdirOp) prefix() string { return "creating" }
|
||||
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
|
||||
func (*MkdirOp) prefix() (string, bool) { return "creating", true }
|
||||
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
|
||||
|
||||
@@ -209,7 +209,7 @@ func (o *MountOverlayOp) Is(op Op) bool {
|
||||
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)
|
||||
}
|
||||
func (*MountOverlayOp) prefix() string { return "mounting" }
|
||||
func (*MountOverlayOp) prefix() (string, bool) { return "mounting", true }
|
||||
func (o *MountOverlayOp) String() string {
|
||||
return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower))
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
tmpPath,
|
||||
target,
|
||||
syscall.MS_RDONLY|syscall.MS_NODEV,
|
||||
false,
|
||||
); err != nil {
|
||||
return err
|
||||
} else if err = k.remove(tmpPath); err != nil {
|
||||
@@ -70,7 +69,7 @@ func (t *TmpfileOp) Is(op Op) bool {
|
||||
t.Path.Is(vt.Path) &&
|
||||
string(t.Data) == string(vt.Data)
|
||||
}
|
||||
func (*TmpfileOp) prefix() string { return "placing" }
|
||||
func (*TmpfileOp) prefix() (string, bool) { return "placing", true }
|
||||
func (t *TmpfileOp) String() string {
|
||||
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
|
||||
}
|
||||
|
||||
@@ -32,5 +32,5 @@ func (p *MountProcOp) Is(op Op) bool {
|
||||
return ok && p.Valid() && vp.Valid() &&
|
||||
p.Target.Is(vp.Target)
|
||||
}
|
||||
func (*MountProcOp) prefix() string { return "mounting" }
|
||||
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }
|
||||
func (*MountProcOp) prefix() (string, bool) { return "mounting", true }
|
||||
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }
|
||||
|
||||
@@ -31,5 +31,5 @@ func (r *RemountOp) Is(op Op) bool {
|
||||
r.Target.Is(vr.Target) &&
|
||||
r.Flags == vr.Flags
|
||||
}
|
||||
func (*RemountOp) prefix() string { return "remounting" }
|
||||
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }
|
||||
func (*RemountOp) prefix() (string, bool) { return "remounting", true }
|
||||
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }
|
||||
|
||||
@@ -55,7 +55,7 @@ func (l *SymlinkOp) Is(op Op) bool {
|
||||
l.LinkName == vl.LinkName &&
|
||||
l.Dereference == vl.Dereference
|
||||
}
|
||||
func (*SymlinkOp) prefix() string { return "creating" }
|
||||
func (*SymlinkOp) prefix() (string, bool) { return "creating", true }
|
||||
func (l *SymlinkOp) String() string {
|
||||
return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName)
|
||||
}
|
||||
|
||||
@@ -56,5 +56,5 @@ func (t *MountTmpfsOp) Is(op Op) bool {
|
||||
t.Size == vt.Size &&
|
||||
t.Perm == vt.Perm
|
||||
}
|
||||
func (*MountTmpfsOp) prefix() string { return "mounting" }
|
||||
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
|
||||
func (*MountTmpfsOp) prefix() (string, bool) { return "mounting", true }
|
||||
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
|
||||
|
||||
+1
-7
@@ -96,15 +96,9 @@ const (
|
||||
)
|
||||
|
||||
// bindMount mounts source on target and recursively applies flags if MS_REC is set.
|
||||
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
|
||||
func (p *procPaths) bindMount(source, target string, flags uintptr) error {
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -12,24 +12,21 @@ import (
|
||||
func TestBindMount(t *testing.T) {
|
||||
checkSimple(t, "bindMount", []simpleTestCase{
|
||||
{"mount", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
|
||||
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
|
||||
}, stub.Expect{Calls: []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil),
|
||||
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)),
|
||||
}}, stub.UniqueError(0xbad)},
|
||||
|
||||
{"success ne", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY, false)
|
||||
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY)
|
||||
}, stub.Expect{Calls: []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"resolved %q on %q flags %#x", []any{"/host/nix", "/sysroot/.host-nix", uintptr(1)}}, nil, nil),
|
||||
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil),
|
||||
call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil),
|
||||
}}, nil},
|
||||
|
||||
{"success", func(k syscallDispatcher) error {
|
||||
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
|
||||
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
|
||||
}, stub.Expect{Calls: []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil),
|
||||
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil),
|
||||
call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil),
|
||||
}}, nil},
|
||||
|
||||
@@ -50,3 +50,18 @@ func (msg *DefaultMsg) Verbosef(format string, v ...any) {
|
||||
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
|
||||
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,3 +146,39 @@ func (out *testOutput) Resume() bool {
|
||||
}
|
||||
|
||||
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") }
|
||||
|
||||
+72
-7
@@ -1,12 +1,77 @@
|
||||
package container
|
||||
|
||||
var msg Msg = new(DefaultMsg)
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func GetOutput() Msg { return msg }
|
||||
func SetOutput(v Msg) {
|
||||
if v == nil {
|
||||
msg = new(DefaultMsg)
|
||||
} else {
|
||||
msg = v
|
||||
const (
|
||||
suspendBufInitial = 1 << 12
|
||||
suspendBufMax = 1 << 24
|
||||
)
|
||||
|
||||
// Suspendable proxies writes to a downstream [io.Writer] but optionally withholds writes
|
||||
// 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) {
|
||||
if !s.s.Load() {
|
||||
return s.Downstream.Write(p)
|
||||
}
|
||||
s.bufOnce.Do(func() { s.buf.Grow(suspendBufInitial) })
|
||||
|
||||
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 (s *Suspendable) IsSuspended() bool { return s.s.Load() }
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
+142
-28
@@ -1,41 +1,155 @@
|
||||
package container
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
)
|
||||
|
||||
func TestGetSetOutput(t *testing.T) {
|
||||
{
|
||||
out := GetOutput()
|
||||
t.Cleanup(func() { SetOutput(out) })
|
||||
func TestSuspendable(t *testing.T) {
|
||||
// copied from output.go
|
||||
const suspendBufMax = 1 << 24
|
||||
|
||||
const (
|
||||
// equivalent to len(want.pt)
|
||||
nSpecialPtEquiv = -iota - 1
|
||||
// equivalent to len(want.w)
|
||||
nSpecialWEquiv
|
||||
// suspends writer before executing test case, implies nSpecialWEquiv
|
||||
nSpecialSuspend
|
||||
// offset: resume writer and measure against dump instead, implies nSpecialPtEquiv
|
||||
nSpecialDump
|
||||
)
|
||||
|
||||
// shares the same writer
|
||||
testCases := []struct {
|
||||
name string
|
||||
w, pt []byte
|
||||
err error
|
||||
wantErr error
|
||||
n int
|
||||
}{
|
||||
{"simple", []byte{0xde, 0xad, 0xbe, 0xef}, []byte{0xde, 0xad, 0xbe, 0xef},
|
||||
nil, nil, nSpecialPtEquiv},
|
||||
|
||||
{"error", []byte{0xb, 0xad}, []byte{0xb, 0xad},
|
||||
stub.UniqueError(0), stub.UniqueError(0), nSpecialPtEquiv},
|
||||
|
||||
{"suspend short", []byte{0}, nil,
|
||||
nil, nil, nSpecialSuspend},
|
||||
{"sw short 0", []byte{0xca, 0xfe, 0xba, 0xbe}, nil,
|
||||
nil, nil, nSpecialWEquiv},
|
||||
{"sw short 1", []byte{0xff}, nil,
|
||||
nil, nil, nSpecialWEquiv},
|
||||
{"resume short", nil, []byte{0, 0xca, 0xfe, 0xba, 0xbe, 0xff}, nil, nil,
|
||||
nSpecialDump},
|
||||
|
||||
{"long pt", bytes.Repeat([]byte{0xff}, suspendBufMax+1), bytes.Repeat([]byte{0xff}, suspendBufMax+1),
|
||||
nil, nil, nSpecialPtEquiv},
|
||||
|
||||
{"suspend fill", bytes.Repeat([]byte{0xfe}, suspendBufMax), nil,
|
||||
nil, nil, nSpecialSuspend},
|
||||
{"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},
|
||||
}
|
||||
|
||||
t.Run("default", func(t *testing.T) {
|
||||
SetOutput(new(stubOutput))
|
||||
if v, ok := GetOutput().(*DefaultMsg); ok {
|
||||
t.Fatalf("SetOutput: got unexpected output %#v", v)
|
||||
}
|
||||
SetOutput(nil)
|
||||
if _, ok := GetOutput().(*DefaultMsg); !ok {
|
||||
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
|
||||
}
|
||||
})
|
||||
var dw expectWriter
|
||||
|
||||
t.Run("stub", func(t *testing.T) {
|
||||
SetOutput(new(stubOutput))
|
||||
if _, ok := GetOutput().(*stubOutput); !ok {
|
||||
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
|
||||
w := container.Suspendable{Downstream: &dw}
|
||||
for _, tc := range testCases {
|
||||
// these share the same writer, so cannot be subtests
|
||||
t.Logf("writing step %q", tc.name)
|
||||
dw.expect, dw.err = tc.pt, tc.err
|
||||
|
||||
var (
|
||||
gotN int
|
||||
gotErr error
|
||||
)
|
||||
|
||||
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)
|
||||
gotN, gotErr = w.Write(tc.w)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type stubOutput struct {
|
||||
wrapF func(error, ...any) error
|
||||
// expectWriter compares Write calls to expect.
|
||||
type expectWriter struct {
|
||||
expect []byte
|
||||
err 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") }
|
||||
func (w *expectWriter) Write(p []byte) (n int, err error) {
|
||||
defer func() { w.expect = nil }()
|
||||
|
||||
n, err = len(p), w.err
|
||||
if w.expect == nil {
|
||||
return 0, errors.New("unexpected call to Write: " + strconv.Quote(string(p)))
|
||||
}
|
||||
if string(p) != string(w.expect) {
|
||||
return 0, errors.New("p = " + strconv.Quote(string(p)) + ", want " + strconv.Quote(string(w.expect)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Generated
+6
-6
@@ -7,11 +7,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1753479839,
|
||||
"narHash": "sha256-E/rPVh7vyPMJUFl2NAew+zibNGfVbANr8BP8nLRbLkQ=",
|
||||
"lastModified": 1756679287,
|
||||
"narHash": "sha256-Xd1vOeY9ccDf5VtVK12yM0FS6qqvfUop8UQlxEB+gTQ=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "0b9bf983db4d064764084cd6748efb1ab8297d1e",
|
||||
"rev": "07fc025fe10487dd80f2ec694f1cd790e752d0e8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -23,11 +23,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1753345091,
|
||||
"narHash": "sha256-CdX2Rtvp5I8HGu9swBmYuq+ILwRxpXdJwlpg8jvN4tU=",
|
||||
"lastModified": 1757020766,
|
||||
"narHash": "sha256-PLoSjHRa2bUbi1x9HoXgTx2AiuzNXs54c8omhadyvp0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3ff0e34b1383648053bba8ed03f201d3466f90c9",
|
||||
"rev": "fe83bbdde2ccdc2cb9573aa846abe8363f79a97a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
)
|
||||
|
||||
func TestContainer(t *testing.T) {
|
||||
t.Run("start empty container", func(t *testing.T) {
|
||||
t.Run("start invalid container", func(t *testing.T) {
|
||||
h := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil)
|
||||
|
||||
wantErr := "container: starting an empty container"
|
||||
wantErr := "container: starting an invalid container"
|
||||
if err := h.Start(); err == nil || err.Error() != wantErr {
|
||||
t.Errorf("Start: error = %v, wantErr %q",
|
||||
err, wantErr)
|
||||
|
||||
+12
-9
@@ -6,8 +6,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -47,6 +49,10 @@ func argFChecked(argsFd, statFd int) (args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
containerTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// 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) {
|
||||
oldWaitDelay := helper.WaitDelay
|
||||
@@ -54,18 +60,15 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
|
||||
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
|
||||
|
||||
t.Run("start helper with status channel and wait", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), containerTimeout)
|
||||
stdout := new(strings.Builder)
|
||||
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) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Fatalf("Wait did not panic")
|
||||
}
|
||||
}()
|
||||
panic(fmt.Sprintf("unreachable: %v", h.Wait()))
|
||||
if err := h.Wait(); !reflect.DeepEqual(err, syscall.EINVAL) &&
|
||||
!reflect.DeepEqual(err, errors.New("exec: not started")) {
|
||||
t.Errorf("Wait: error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Log("starting helper stub")
|
||||
@@ -108,7 +111,7 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
|
||||
})
|
||||
|
||||
t.Run("start helper and wait", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), containerTimeout)
|
||||
defer cancel()
|
||||
stdout := new(strings.Builder)
|
||||
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false)
|
||||
|
||||
+30
@@ -2,12 +2,42 @@
|
||||
package hst
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/system"
|
||||
"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.
|
||||
type Paths struct {
|
||||
// temporary directory returned by [os.TempDir] (usually `/tmp`)
|
||||
|
||||
@@ -2,11 +2,93 @@ package hst_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
"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) {
|
||||
const want = `{
|
||||
"id": "org.chromium.Chromium",
|
||||
|
||||
+12
-66
@@ -3,80 +3,26 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"os"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type App struct {
|
||||
outcome *Outcome
|
||||
|
||||
id *stringPair[state.ID]
|
||||
sys sys.State
|
||||
ctx context.Context
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// ID returns a copy of [state.ID] held by App.
|
||||
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)"
|
||||
// Main runs an app according to [hst.Config] and terminates. Main does not return.
|
||||
func Main(ctx context.Context, config *hst.Config) {
|
||||
var id state.ID
|
||||
if err := state.NewAppID(&id); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
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)
|
||||
seal := outcome{id: &stringPair[state.ID]{id, id.String()}, syscallDispatcher: direct{}}
|
||||
if err := seal.finalise(ctx, config); err != nil {
|
||||
printMessageError("cannot seal app:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("(unsealed app %s)", a.id)
|
||||
}
|
||||
|
||||
// Seal determines the [Outcome] of [hst.Config].
|
||||
// Values stored in and referred to by [hst.Config] might be overwritten and must not be used again.
|
||||
func (a *App) Seal(config *hst.Config) (*Outcome, 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
|
||||
seal.main()
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
package app_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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(context.TODO(), 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("/dev/shm"), 0, 01777).
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
package app_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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(context.TODO(), 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("/dev/shm"), 0, 01777).
|
||||
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(context.TODO(), 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("/dev/shm"), 0, 01777).
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,34 +1,24 @@
|
||||
package app_test
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os/exec"
|
||||
"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 {
|
||||
lookPathErr map[string]error
|
||||
usernameErr map[string]error
|
||||
}
|
||||
|
||||
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) new(func(k syscallDispatcher)) { panic("not implemented") }
|
||||
|
||||
func (s *stubNixOS) Println(v ...any) { log.Println(v...) }
|
||||
func (s *stubNixOS) Printf(format string, v ...any) { log.Printf(format, v...) }
|
||||
func (k *stubNixOS) getuid() int { return 1971 }
|
||||
func (k *stubNixOS) getgid() int { return 100 }
|
||||
|
||||
func (s *stubNixOS) LookupEnv(key string) (string, bool) {
|
||||
func (k *stubNixOS) lookupEnv(key string) (string, bool) {
|
||||
switch key {
|
||||
case "SHELL":
|
||||
return "/run/current-system/sw/bin/zsh", true
|
||||
@@ -40,6 +30,8 @@ func (s *stubNixOS) LookupEnv(key string) (string, bool) {
|
||||
return "", false
|
||||
case "HOME":
|
||||
return "/home/ophestra", true
|
||||
case "XDG_RUNTIME_DIR":
|
||||
return "/run/user/1971", true
|
||||
case "XDG_CONFIG_HOME":
|
||||
return "/home/ophestra/xdg/config", true
|
||||
default:
|
||||
@@ -47,61 +39,7 @@ func (s *stubNixOS) LookupEnv(key string) (string, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
func (k *stubNixOS) stat(name string) (fs.FileInfo, error) {
|
||||
switch name {
|
||||
case "/var/run/nscd":
|
||||
return nil, nil
|
||||
@@ -118,17 +56,144 @@ func (s *stubNixOS) Stat(name string) (fs.FileInfo, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stubNixOS) Open(name string) (fs.File, error) {
|
||||
func (k *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 open unexpected file %q", name))
|
||||
panic(fmt.Sprintf("attempted to read unexpected directory %q", name))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stubNixOS) Paths() hst.Paths {
|
||||
return hst.Paths{
|
||||
SharePath: m("/tmp/hakurei.1971"),
|
||||
RuntimePath: m("/run/user/1971"),
|
||||
RunDirPath: m("/run/user/1971/hakurei"),
|
||||
func (k *stubNixOS) tempdir() string { return "/tmp/" }
|
||||
|
||||
func (k *stubNixOS) evalSymlinks(path string) (string, error) {
|
||||
switch path {
|
||||
case "/run/user/1971":
|
||||
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...) }
|
||||
+386
-40
@@ -1,64 +1,402 @@
|
||||
package app_test
|
||||
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"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
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...)
|
||||
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) {
|
||||
a := app.NewWithID(t.Context(), 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
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("sys", func(t *testing.T) {
|
||||
if !seal.sys.Equal(tc.wantSys) {
|
||||
t.Errorf("Seal: sys = %#v, want %#v", seal.sys, 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))
|
||||
}
|
||||
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))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -104,3 +442,11 @@ 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}
|
||||
}
|
||||
|
||||
+26
-24
@@ -11,8 +11,6 @@ import (
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
@@ -21,9 +19,15 @@ const preallocateOpsCount = 1 << 5
|
||||
|
||||
// newContainer initialises [container.Params] via [hst.ContainerConfig].
|
||||
// Note that remaining container setup must be queued by the caller.
|
||||
func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid *int) (*container.Params, map[string]string, error) {
|
||||
func newContainer(
|
||||
k syscallDispatcher,
|
||||
s *hst.ContainerConfig,
|
||||
prefix string,
|
||||
sc *hst.Paths,
|
||||
uid, gid *int,
|
||||
) (*container.Params, map[string]string, error) {
|
||||
if s == nil {
|
||||
return nil, nil, hlog.WrapErr(syscall.EBADE, "invalid container configuration")
|
||||
return nil, nil, newWithMessage("invalid container configuration")
|
||||
}
|
||||
|
||||
params := &container.Params{
|
||||
@@ -39,9 +43,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
||||
ForwardCancel: s.WaitDelay >= 0,
|
||||
}
|
||||
|
||||
as := &hst.ApplyState{
|
||||
AutoEtcPrefix: prefix,
|
||||
}
|
||||
as := &hst.ApplyState{AutoEtcPrefix: prefix}
|
||||
{
|
||||
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem))
|
||||
params.Ops = &ops
|
||||
@@ -66,13 +68,13 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
||||
}
|
||||
|
||||
if s.MapRealUID {
|
||||
params.Uid = os.Getuid()
|
||||
params.Uid = k.getuid()
|
||||
*uid = params.Uid
|
||||
params.Gid = os.Getgid()
|
||||
params.Gid = k.getgid()
|
||||
*gid = params.Gid
|
||||
} else {
|
||||
*uid = container.OverflowUid()
|
||||
*gid = container.OverflowGid()
|
||||
*uid = k.overflowUid()
|
||||
*gid = k.overflowGid()
|
||||
}
|
||||
|
||||
filesystem := s.Filesystem
|
||||
@@ -99,6 +101,8 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
||||
} else {
|
||||
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;
|
||||
|
||||
@@ -106,7 +110,6 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
||||
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 */
|
||||
var hidePaths []string
|
||||
sc := os.Paths()
|
||||
hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String())
|
||||
_, systemBusAddr := dbus.Address()
|
||||
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
|
||||
@@ -123,11 +126,11 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
||||
// get parent dir of socket
|
||||
dir := path.Dir(pair[1])
|
||||
if dir == "." || dir == container.FHSRoot {
|
||||
os.Printf("dbus socket %q is in an unusual location", pair[1])
|
||||
k.verbosef("dbus socket %q is in an unusual location", pair[1])
|
||||
}
|
||||
hidePaths = append(hidePaths, dir)
|
||||
} else {
|
||||
os.Printf("dbus socket %q is not absolute", pair[1])
|
||||
k.verbosef("dbus socket %q is not absolute", pair[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,7 +138,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
||||
}
|
||||
hidePathMatch := make([]bool, len(hidePaths))
|
||||
for i := range hidePaths {
|
||||
if err := evalSymlinks(os, &hidePaths[i]); err != nil {
|
||||
if err := evalSymlinks(k, &hidePaths[i]); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
@@ -154,7 +157,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
||||
// AutoRootOp is a collection of many BindMountOp internally
|
||||
var autoRootEntries []fs.DirEntry
|
||||
if autoroot != nil {
|
||||
if d, err := os.ReadDir(autoroot.Source.String()); err != nil {
|
||||
if d, err := k.readdir(autoroot.Source.String()); err != nil {
|
||||
return nil, nil, err
|
||||
} else {
|
||||
// autoroot counter
|
||||
@@ -190,7 +193,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
||||
}
|
||||
|
||||
hidePathSourceEval[i] = [2]string{a.String(), a.String()}
|
||||
if err := evalSymlinks(os, &hidePathSourceEval[i][0]); err != nil {
|
||||
if err := evalSymlinks(k, &hidePathSourceEval[i][0]); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
@@ -206,7 +209,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
||||
return nil, nil, err
|
||||
} else if ok {
|
||||
hidePathMatch[i] = true
|
||||
os.Printf("hiding path %q from %q", hidePaths[i], p[1])
|
||||
k.verbosef("hiding path %q from %q", hidePaths[i], p[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,20 +234,19 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
||||
|
||||
// no more ContainerConfig paths beyond this point
|
||||
if !s.Device {
|
||||
params.
|
||||
Remount(container.AbsFHSDev, syscall.MS_RDONLY).
|
||||
Tmpfs(container.AbsFHSDev.Append("shm"), 0, 01777)
|
||||
params.Remount(container.AbsFHSDev, syscall.MS_RDONLY)
|
||||
}
|
||||
|
||||
return params, maps.Clone(s.Env), nil
|
||||
}
|
||||
|
||||
func evalSymlinks(os sys.State, v *string) error {
|
||||
if p, err := os.EvalSymlinks(*v); err != nil {
|
||||
// evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist].
|
||||
func evalSymlinks(k syscallDispatcher, v *string) error {
|
||||
if p, err := k.evalSymlinks(*v); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
os.Printf("path %q does not yet exist", *v)
|
||||
k.verbosef("path %q does not yet exist", *v)
|
||||
} else {
|
||||
*v = p
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
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...) }
|
||||
@@ -1,181 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
func NewWithID(ctx context.Context, id state.ID, os sys.State) *App {
|
||||
return &App{id: newID(&id), sys: os, ctx: ctx}
|
||||
}
|
||||
|
||||
func AppIParams(a *App, seal *Outcome) (*system.I, *container.Params) {
|
||||
if a.outcome != seal || a.id != seal.id {
|
||||
panic("broken app/outcome link")
|
||||
}
|
||||
return seal.sys, seal.container
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"os/user"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -21,45 +21,19 @@ import (
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/system/wayland"
|
||||
)
|
||||
|
||||
const (
|
||||
home = "HOME"
|
||||
shell = "SHELL"
|
||||
func newWithMessage(msg string) error { return newWithMessageError(msg, os.ErrInvalid) }
|
||||
func newWithMessageError(msg string, err error) error {
|
||||
return &hst.AppError{Step: "finalise", Err: err, Msg: msg}
|
||||
}
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
// An Outcome is the runnable state of a hakurei container via [hst.Config].
|
||||
type Outcome struct {
|
||||
// An outcome is the runnable state of a hakurei container via [hst.Config].
|
||||
type outcome struct {
|
||||
// copied from initialising [app]
|
||||
id *stringPair[state.ID]
|
||||
// copied from [sys.State]
|
||||
@@ -79,8 +53,9 @@ type Outcome struct {
|
||||
container *container.Params
|
||||
env map[string]string
|
||||
sync *os.File
|
||||
active atomic.Bool
|
||||
|
||||
f atomic.Bool
|
||||
syscallDispatcher
|
||||
}
|
||||
|
||||
// shareHost holds optional share directory state that must not be accessed directly
|
||||
@@ -92,7 +67,7 @@ type shareHost struct {
|
||||
// process-specific directory in XDG_RUNTIME_DIR, empty if unused
|
||||
runtimeSharePath *container.Absolute
|
||||
|
||||
seal *Outcome
|
||||
seal *outcome
|
||||
sc hst.Paths
|
||||
}
|
||||
|
||||
@@ -145,61 +120,82 @@ type hsuUser struct {
|
||||
username string
|
||||
}
|
||||
|
||||
func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error {
|
||||
func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
|
||||
const (
|
||||
home = "HOME"
|
||||
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 seal.ctx != nil {
|
||||
if k.ctx != nil {
|
||||
// unreachable
|
||||
panic("attempting to finalise twice")
|
||||
}
|
||||
seal.ctx = ctx
|
||||
k.ctx = ctx
|
||||
|
||||
if config == nil {
|
||||
return hlog.WrapErr(syscall.EINVAL, syscall.EINVAL.Error())
|
||||
return newWithMessage("invalid configuration")
|
||||
}
|
||||
if config.Home == nil {
|
||||
return hlog.WrapErr(os.ErrInvalid, "invalid path to home directory")
|
||||
return newWithMessage("invalid path to home directory")
|
||||
}
|
||||
|
||||
{
|
||||
// encode initial configuration for state tracking
|
||||
ct := new(bytes.Buffer)
|
||||
if err := gob.NewEncoder(ct).Encode(config); err != nil {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot encode initial config:")
|
||||
return &hst.AppError{Step: "encode initial config", Err: err}
|
||||
}
|
||||
seal.ct = ct
|
||||
k.ct = ct
|
||||
}
|
||||
|
||||
// allowed identity range 0 to 9999, this is checked again in hsu
|
||||
if config.Identity < 0 || config.Identity > 9999 {
|
||||
return hlog.WrapErr(ErrIdent,
|
||||
fmt.Sprintf("identity %d out of range", config.Identity))
|
||||
return newWithMessage(fmt.Sprintf("identity %d out of range", config.Identity))
|
||||
}
|
||||
|
||||
seal.user = hsuUser{
|
||||
k.user = hsuUser{
|
||||
identity: newInt(config.Identity),
|
||||
home: config.Home,
|
||||
username: config.Username,
|
||||
}
|
||||
if seal.user.username == "" {
|
||||
seal.user.username = "chronos"
|
||||
} else if !isValidUsername(seal.user.username) {
|
||||
return hlog.WrapErr(ErrName,
|
||||
fmt.Sprintf("invalid user name %q", seal.user.username))
|
||||
|
||||
hsu := Hsu{k: k}
|
||||
if k.user.username == "" {
|
||||
k.user.username = "chronos"
|
||||
} else if !isValidUsername(k.user.username) {
|
||||
return newWithMessage(fmt.Sprintf("invalid user name %q", k.user.username))
|
||||
}
|
||||
if u, err := sys.Uid(seal.user.identity.unwrap()); err != nil {
|
||||
return err
|
||||
} else {
|
||||
seal.user.uid = newInt(u)
|
||||
}
|
||||
seal.user.supp = make([]string, len(config.Groups))
|
||||
k.user.uid = newInt(HsuUid(hsu.MustID(), k.user.identity.unwrap()))
|
||||
|
||||
k.user.supp = make([]string, len(config.Groups))
|
||||
for i, name := range config.Groups {
|
||||
if g, err := sys.LookupGroup(name); err != nil {
|
||||
return hlog.WrapErr(err,
|
||||
fmt.Sprintf("unknown group %q", name))
|
||||
if gid, err := k.lookupGroupId(name); err != nil {
|
||||
var unknownGroupError user.UnknownGroupError
|
||||
if errors.As(err, &unknownGroupError) {
|
||||
return newWithMessageError(fmt.Sprintf("unknown group %q", name), unknownGroupError)
|
||||
} else {
|
||||
return &hst.AppError{Step: "look up group by name", Err: err}
|
||||
}
|
||||
} else {
|
||||
seal.user.supp[i] = g.Gid
|
||||
k.user.supp[i] = gid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +205,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
||||
|
||||
if config.Shell == nil {
|
||||
config.Shell = container.AbsFHSRoot.Append("bin", "sh")
|
||||
s, _ := sys.LookupEnv(shell)
|
||||
s, _ := k.lookupEnv(shell)
|
||||
if a, err := container.NewAbs(s); err == nil {
|
||||
config.Shell = a
|
||||
}
|
||||
@@ -218,10 +214,10 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
||||
// hsu clears the environment so resolve paths early
|
||||
if config.Path == nil {
|
||||
if len(config.Args) > 0 {
|
||||
if p, err := sys.LookPath(config.Args[0]); err != nil {
|
||||
return hlog.WrapErr(err, err.Error())
|
||||
if p, err := k.lookPath(config.Args[0]); err != nil {
|
||||
return &hst.AppError{Step: "look up executable file", Err: err}
|
||||
} else if config.Path, err = container.NewAbs(p); err != nil {
|
||||
return hlog.WrapErr(err, err.Error())
|
||||
return newWithMessageError(err.Error(), err)
|
||||
}
|
||||
} else {
|
||||
config.Path = config.Shell
|
||||
@@ -254,7 +250,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
||||
|
||||
// hide nscd from container if present
|
||||
nscd := container.AbsFHSVar.Append("run/nscd")
|
||||
if _, err := sys.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) {
|
||||
if _, err := k.stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) {
|
||||
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}})
|
||||
}
|
||||
|
||||
@@ -272,93 +268,95 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
||||
|
||||
// late nil checks for pd behaviour
|
||||
if config.Shell == nil {
|
||||
return hlog.WrapErr(syscall.EINVAL, "invalid shell path")
|
||||
return newWithMessage("invalid shell path")
|
||||
}
|
||||
if config.Path == nil {
|
||||
return hlog.WrapErr(syscall.EINVAL, "invalid program path")
|
||||
return newWithMessage("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 uid, gid int
|
||||
var err error
|
||||
seal.container, seal.env, err = newContainer(config.Container, sys, seal.id.String(), &uid, &gid)
|
||||
seal.waitDelay = config.Container.WaitDelay
|
||||
k.container, k.env, err = newContainer(k, config.Container, k.id.String(), &share.sc, &uid, &gid)
|
||||
k.waitDelay = config.Container.WaitDelay
|
||||
if err != nil {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot initialise container configuration:")
|
||||
return &hst.AppError{Step: "initialise container configuration", Err: err}
|
||||
}
|
||||
if len(config.Args) == 0 {
|
||||
config.Args = []string{config.Path.String()}
|
||||
}
|
||||
seal.container.Path = config.Path
|
||||
seal.container.Args = config.Args
|
||||
k.container.Path = config.Path
|
||||
k.container.Args = config.Args
|
||||
|
||||
mapuid = newInt(uid)
|
||||
mapgid = newInt(gid)
|
||||
if seal.env == nil {
|
||||
seal.env = make(map[string]string, 1<<6)
|
||||
if k.env == nil {
|
||||
k.env = make(map[string]string, 1<<6)
|
||||
}
|
||||
}
|
||||
|
||||
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
|
||||
innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String())
|
||||
seal.env[xdgRuntimeDir] = innerRuntimeDir.String()
|
||||
seal.env[xdgSessionClass] = "user"
|
||||
seal.env[xdgSessionType] = "tty"
|
||||
k.env[xdgRuntimeDir] = innerRuntimeDir.String()
|
||||
k.env[xdgSessionClass] = "user"
|
||||
k.env[xdgSessionType] = "tty"
|
||||
|
||||
share := &shareHost{seal: seal, sc: sys.Paths()}
|
||||
seal.runDirPath = share.sc.RunDirPath
|
||||
seal.sys = system.New(seal.ctx, seal.user.uid.unwrap())
|
||||
seal.sys.Ensure(share.sc.SharePath.String(), 0711)
|
||||
k.runDirPath = share.sc.RunDirPath
|
||||
k.sys = system.New(k.ctx, k.user.uid.unwrap())
|
||||
k.sys.Ensure(share.sc.SharePath.String(), 0711)
|
||||
|
||||
{
|
||||
runtimeDir := share.sc.SharePath.Append("runtime")
|
||||
seal.sys.Ensure(runtimeDir.String(), 0700)
|
||||
seal.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute)
|
||||
runtimeDirInst := runtimeDir.Append(seal.user.identity.String())
|
||||
seal.sys.Ensure(runtimeDirInst.String(), 0700)
|
||||
seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute)
|
||||
seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
|
||||
seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
|
||||
k.sys.Ensure(runtimeDir.String(), 0700)
|
||||
k.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute)
|
||||
runtimeDirInst := runtimeDir.Append(k.user.identity.String())
|
||||
k.sys.Ensure(runtimeDirInst.String(), 0700)
|
||||
k.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute)
|
||||
k.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
|
||||
k.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
|
||||
}
|
||||
|
||||
{
|
||||
tmpdir := share.sc.SharePath.Append("tmpdir")
|
||||
seal.sys.Ensure(tmpdir.String(), 0700)
|
||||
seal.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute)
|
||||
tmpdirInst := tmpdir.Append(seal.user.identity.String())
|
||||
seal.sys.Ensure(tmpdirInst.String(), 01700)
|
||||
seal.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute)
|
||||
k.sys.Ensure(tmpdir.String(), 0700)
|
||||
k.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute)
|
||||
tmpdirInst := tmpdir.Append(k.user.identity.String())
|
||||
k.sys.Ensure(tmpdirInst.String(), 01700)
|
||||
k.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
|
||||
seal.container.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable)
|
||||
k.container.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable)
|
||||
}
|
||||
|
||||
{
|
||||
username := "chronos"
|
||||
if seal.user.username != "" {
|
||||
username = seal.user.username
|
||||
if k.user.username != "" {
|
||||
username = k.user.username
|
||||
}
|
||||
seal.container.Dir = seal.user.home
|
||||
seal.env["HOME"] = seal.user.home.String()
|
||||
seal.env["USER"] = username
|
||||
seal.env[shell] = config.Shell.String()
|
||||
k.container.Dir = k.user.home
|
||||
k.env["HOME"] = k.user.home.String()
|
||||
k.env["USER"] = username
|
||||
k.env[shell] = config.Shell.String()
|
||||
|
||||
seal.container.Place(container.AbsFHSEtc.Append("passwd"),
|
||||
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+seal.user.home.String()+":"+config.Shell.String()+"\n"))
|
||||
seal.container.Place(container.AbsFHSEtc.Append("group"),
|
||||
k.container.Place(container.AbsFHSEtc.Append("passwd"),
|
||||
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+k.user.home.String()+":"+config.Shell.String()+"\n"))
|
||||
k.container.Place(container.AbsFHSEtc.Append("group"),
|
||||
[]byte("hakurei:x:"+mapgid.String()+":\n"))
|
||||
}
|
||||
|
||||
// pass TERM for proper terminal I/O in initial process
|
||||
if t, ok := sys.LookupEnv(term); ok {
|
||||
seal.env[term] = t
|
||||
if t, ok := k.lookupEnv(term); ok {
|
||||
k.env[term] = t
|
||||
}
|
||||
|
||||
if config.Enablements.Unwrap()&system.EWayland != 0 {
|
||||
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
|
||||
var socketPath *container.Absolute
|
||||
if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok {
|
||||
if name, ok := k.lookupEnv(wayland.WaylandDisplay); !ok {
|
||||
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
|
||||
socketPath = share.sc.RuntimePath.Append(wayland.FallbackName)
|
||||
} else if a, err := container.NewAbs(name); err != nil {
|
||||
@@ -368,30 +366,29 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
||||
}
|
||||
|
||||
innerPath := innerRuntimeDir.Append(wayland.FallbackName)
|
||||
seal.env[wayland.WaylandDisplay] = wayland.FallbackName
|
||||
k.env[wayland.WaylandDisplay] = wayland.FallbackName
|
||||
|
||||
if !config.DirectWayland { // set up security-context-v1
|
||||
appID := config.ID
|
||||
if appID == "" {
|
||||
// use instance ID in case app id is not set
|
||||
appID = "app.hakurei." + seal.id.String()
|
||||
appID = "app.hakurei." + k.id.String()
|
||||
}
|
||||
// downstream socket paths
|
||||
outerPath := share.instance().Append("wayland")
|
||||
seal.sys.Wayland(&seal.sync, outerPath.String(), socketPath.String(), appID, seal.id.String())
|
||||
seal.container.Bind(outerPath, innerPath, 0)
|
||||
k.sys.Wayland(&k.sync, outerPath.String(), socketPath.String(), appID, k.id.String())
|
||||
k.container.Bind(outerPath, innerPath, 0)
|
||||
} else { // bind mount wayland socket (insecure)
|
||||
hlog.Verbose("direct wayland access, PROCEED WITH CAUTION")
|
||||
share.ensureRuntimeDir()
|
||||
seal.container.Bind(socketPath, innerPath, 0)
|
||||
seal.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute)
|
||||
k.container.Bind(socketPath, innerPath, 0)
|
||||
k.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Enablements.Unwrap()&system.EX11 != 0 {
|
||||
if d, ok := sys.LookupEnv(display); !ok {
|
||||
return hlog.WrapErr(ErrXDisplay,
|
||||
"DISPLAY is not set")
|
||||
if d, ok := k.lookupEnv(display); !ok {
|
||||
return newWithMessage("DISPLAY is not set")
|
||||
} else {
|
||||
socketDir := container.AbsFHSTmp.Append(".X11-unix")
|
||||
|
||||
@@ -408,22 +405,21 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
||||
}
|
||||
}
|
||||
if socketPath != nil {
|
||||
if _, err := sys.Stat(socketPath.String()); err != nil {
|
||||
if _, err := k.stat(socketPath.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot access X11 socket %q:", socketPath))
|
||||
return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err}
|
||||
}
|
||||
} else {
|
||||
seal.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute)
|
||||
k.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute)
|
||||
if !config.Container.HostAbstract {
|
||||
d = "unix:" + socketPath.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
seal.sys.ChangeHosts("#" + seal.user.uid.String())
|
||||
seal.env[display] = d
|
||||
seal.container.Bind(socketDir, socketDir, 0)
|
||||
k.sys.ChangeHosts("#" + k.user.uid.String())
|
||||
k.env[display] = d
|
||||
k.container.Bind(socketDir, socketDir, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,46 +429,101 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
||||
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
|
||||
pulseSocket := pulseRuntimeDir.Append("native")
|
||||
|
||||
if _, err := sys.Stat(pulseRuntimeDir.String()); err != nil {
|
||||
if _, err := k.stat(pulseRuntimeDir.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir))
|
||||
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err}
|
||||
}
|
||||
return hlog.WrapErr(ErrPulseSocket,
|
||||
fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
|
||||
return newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
|
||||
}
|
||||
|
||||
if s, err := sys.Stat(pulseSocket.String()); err != nil {
|
||||
if s, err := k.stat(pulseSocket.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket))
|
||||
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err}
|
||||
}
|
||||
return hlog.WrapErr(ErrPulseSocket,
|
||||
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
|
||||
return newWithMessage(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
|
||||
} else {
|
||||
if m := s.Mode(); m&0o006 != 0o006 {
|
||||
return hlog.WrapErr(ErrPulseMode,
|
||||
fmt.Sprintf("unexpected permissions on %q:", pulseSocket), m)
|
||||
return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m))
|
||||
}
|
||||
}
|
||||
|
||||
// hard link pulse socket into target-executable share
|
||||
innerPulseRuntimeDir := share.runtime().Append("pulse")
|
||||
innerPulseSocket := innerRuntimeDir.Append("pulse", "native")
|
||||
seal.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String())
|
||||
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
|
||||
seal.env[pulseServer] = "unix:" + innerPulseSocket.String()
|
||||
k.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String())
|
||||
k.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
|
||||
k.env[pulseServer] = "unix:" + innerPulseSocket.String()
|
||||
|
||||
// publish current user's pulse cookie for target user
|
||||
if src, err := discoverPulseCookie(sys); err != nil {
|
||||
// not fatal
|
||||
hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message()))
|
||||
} else {
|
||||
var paCookiePath *container.Absolute
|
||||
{
|
||||
const paLocateStep = "locate PulseAudio cookie"
|
||||
|
||||
// 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")
|
||||
seal.env[pulseCookie] = innerDst.String()
|
||||
k.env[pulseCookie] = innerDst.String()
|
||||
var payload *[]byte
|
||||
seal.container.PlaceP(innerDst, &payload)
|
||||
seal.sys.CopyFile(payload, src, 256, 256)
|
||||
k.container.PlaceP(innerDst, &payload)
|
||||
k.sys.CopyFile(payload, paCookiePath.String(), 256, 256)
|
||||
} else {
|
||||
hlog.Verbose("cannot locate PulseAudio cookie (tried " +
|
||||
"$PULSE_COOKIE, " +
|
||||
"$XDG_CONFIG_HOME/pulse/cookie, " +
|
||||
"$HOME/.pulse-cookie)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,30 +537,30 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
||||
sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket")
|
||||
|
||||
// configure dbus proxy
|
||||
if f, err := seal.sys.ProxyDBus(
|
||||
if f, err := k.sys.ProxyDBus(
|
||||
config.SessionBus, config.SystemBus,
|
||||
sessionPath.String(), systemPath.String(),
|
||||
); err != nil {
|
||||
return err
|
||||
} else {
|
||||
seal.dbusMsg = f
|
||||
k.dbusMsg = f
|
||||
}
|
||||
|
||||
// share proxy sockets
|
||||
sessionInner := innerRuntimeDir.Append("bus")
|
||||
seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String()
|
||||
seal.container.Bind(sessionPath, sessionInner, 0)
|
||||
seal.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write)
|
||||
k.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String()
|
||||
k.container.Bind(sessionPath, sessionInner, 0)
|
||||
k.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write)
|
||||
if config.SystemBus != nil {
|
||||
systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket")
|
||||
seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String()
|
||||
seal.container.Bind(systemPath, systemInner, 0)
|
||||
seal.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write)
|
||||
k.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String()
|
||||
k.container.Bind(systemPath, systemInner, 0)
|
||||
k.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write)
|
||||
}
|
||||
}
|
||||
|
||||
// mount root read-only as the final setup Op
|
||||
seal.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY)
|
||||
k.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY)
|
||||
|
||||
// append ExtraPerms last
|
||||
for _, p := range config.ExtraPerms {
|
||||
@@ -518,7 +569,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
||||
}
|
||||
|
||||
if p.Ensure {
|
||||
seal.sys.Ensure(p.Path.String(), 0700)
|
||||
k.sys.Ensure(p.Path.String(), 0700)
|
||||
}
|
||||
|
||||
perms := make(acl.Perms, 0, 3)
|
||||
@@ -531,63 +582,24 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
||||
if p.Execute {
|
||||
perms = append(perms, acl.Execute)
|
||||
}
|
||||
seal.sys.UpdatePermType(system.User, p.Path.String(), perms...)
|
||||
k.sys.UpdatePermType(system.User, p.Path.String(), perms...)
|
||||
}
|
||||
|
||||
// flatten and sort env for deterministic behaviour
|
||||
seal.container.Env = make([]string, 0, len(seal.env))
|
||||
for k, v := range seal.env {
|
||||
if strings.IndexByte(k, '=') != -1 {
|
||||
return hlog.WrapErr(syscall.EINVAL,
|
||||
fmt.Sprintf("invalid environment variable %s", k))
|
||||
k.container.Env = make([]string, 0, len(k.env))
|
||||
for key, value := range k.env {
|
||||
if strings.IndexByte(key, '=') != -1 {
|
||||
return &hst.AppError{Step: "flatten environment", Err: syscall.EINVAL,
|
||||
Msg: fmt.Sprintf("invalid environment variable %s", key)}
|
||||
}
|
||||
seal.container.Env = append(seal.container.Env, k+"="+v)
|
||||
k.container.Env = append(k.container.Env, key+"="+value)
|
||||
}
|
||||
slices.Sort(seal.container.Env)
|
||||
slices.Sort(k.container.Env)
|
||||
|
||||
if hlog.Load() {
|
||||
hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
|
||||
seal.user.uid, seal.user.username, config.Groups, seal.container.Args, len(*seal.container.Ops))
|
||||
k.user.uid, k.user.username, config.Groups, k.container.Args, len(*k.container.Ops))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
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 }
|
||||
@@ -0,0 +1,36 @@
|
||||
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)
|
||||
}
|
||||
+256
-146
@@ -19,82 +19,169 @@ import (
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
// duration to wait for shim to exit, after container WaitDelay has elapsed.
|
||||
const shimWaitTimeout = 5 * time.Second
|
||||
|
||||
// RunState stores the outcome of a call to [Outcome.Run].
|
||||
type RunState struct {
|
||||
// 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
|
||||
// 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
|
||||
store state.Store
|
||||
cancel context.CancelFunc
|
||||
cmd *exec.Cmd
|
||||
cmdWait chan error
|
||||
|
||||
k *outcome
|
||||
uintptr
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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
|
||||
)
|
||||
|
||||
// Run commits deferred system setup and starts the container.
|
||||
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")
|
||||
// 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()
|
||||
}
|
||||
|
||||
if rs == nil {
|
||||
panic("invalid state")
|
||||
var hasErr bool
|
||||
// updates hasErr but does not terminate
|
||||
perror := func(err error, message string) {
|
||||
hasErr = true
|
||||
printMessageError("cannot "+message+":", err)
|
||||
}
|
||||
|
||||
// read comp value early to allow for early failure
|
||||
hsuPath := internal.MustHsuPath()
|
||||
|
||||
if err := seal.sys.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
store := state.NewMulti(seal.runDirPath.String())
|
||||
deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store
|
||||
exitCode := 1
|
||||
defer func() {
|
||||
var revertErr error
|
||||
storeErr := new(StateStoreError)
|
||||
storeErr.Inner, storeErr.DoErr = store.Do(seal.user.identity.unwrap(), func(c state.Cursor) {
|
||||
revertErr = func() error {
|
||||
storeErr.InnerErr = deferredStoreFunc(c)
|
||||
if hasErr {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
}()
|
||||
|
||||
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)
|
||||
// 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())
|
||||
|
||||
// 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)
|
||||
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 {
|
||||
@@ -102,128 +189,151 @@ func (seal *Outcome) Run(rs *RunState) error {
|
||||
}
|
||||
}
|
||||
|
||||
return seal.sys.Revert((*system.Criteria)(&ec))
|
||||
}()
|
||||
})
|
||||
storeErr.save(revertErr, store.Close())
|
||||
rs.RevertErr = storeErr.equiv("error during cleanup:")
|
||||
}()
|
||||
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")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(seal.ctx)
|
||||
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()
|
||||
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
|
||||
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
|
||||
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
|
||||
ms.cmd.Cancel = func() error { return ms.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:")
|
||||
if fd, encoder, err := container.Setup(&ms.cmd.ExtraFiles); err != nil {
|
||||
ms.fatal("cannot create shim setup pipe:", err)
|
||||
} else {
|
||||
e = encoder
|
||||
cmd.Env = []string{
|
||||
ms.cmd.Env = []string{
|
||||
// passed through to shim by hsu
|
||||
shimEnv + "=" + strconv.Itoa(fd),
|
||||
// interpreted by hsu
|
||||
"HAKUREI_APP_ID=" + seal.user.identity.String(),
|
||||
"HAKUREI_IDENTITY=" + k.user.identity.String(),
|
||||
}
|
||||
}
|
||||
|
||||
if len(seal.user.supp) > 0 {
|
||||
hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp)
|
||||
if len(k.user.supp) > 0 {
|
||||
hlog.Verbosef("attaching supplementary group ids %s", k.user.supp)
|
||||
// interpreted by hsu
|
||||
cmd.Env = append(cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " "))
|
||||
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 := cmd.Start(); err != nil {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot start setuid wrapper:")
|
||||
if err := ms.cmd.Start(); err != nil {
|
||||
ms.fatal("cannot start setuid wrapper:", err)
|
||||
}
|
||||
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(),
|
||||
})
|
||||
}()
|
||||
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 := <-setupErr:
|
||||
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()
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot transmit shim config:")
|
||||
ms.fatal("cannot transmit shim config:", err)
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
hlog.Resume()
|
||||
return hlog.WrapErr(syscall.ECANCELED,
|
||||
"shim setup canceled")
|
||||
ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err()))
|
||||
}
|
||||
|
||||
// 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,
|
||||
// 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)
|
||||
}
|
||||
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.identity.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)
|
||||
}); err != nil {
|
||||
if ok {
|
||||
ms.uintptr |= mainNeedsDestroy
|
||||
ms.fatal("cannot unlock state store:", err)
|
||||
} else {
|
||||
ms.fatal("cannot open state store:", err)
|
||||
}
|
||||
}
|
||||
if seal.dbusMsg != nil {
|
||||
seal.dbusMsg()
|
||||
}
|
||||
// state in store at this point, destroy defunct state entry on termination
|
||||
ms.uintptr |= mainNeedsDestroy
|
||||
|
||||
return earlyStoreErr.equiv("cannot save process state:")
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -45,8 +45,10 @@ const (
|
||||
// ShimExitOrphan is returned when the shim is orphaned before monitor delivers a signal.
|
||||
ShimExitOrphan = 3
|
||||
|
||||
// DefaultShimWaitDelay is used when WaitDelay has its zero value.
|
||||
DefaultShimWaitDelay = 5 * time.Second
|
||||
MaxShimWaitDelay = 30 * time.Second
|
||||
// MaxShimWaitDelay is used instead if WaitDelay exceeds its value.
|
||||
MaxShimWaitDelay = 30 * time.Second
|
||||
)
|
||||
|
||||
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
|
||||
@@ -155,11 +157,11 @@ func ShimMain() {
|
||||
}
|
||||
|
||||
if err := z.Start(); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot start container:")
|
||||
printMessageError("cannot start container:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := z.Serve(); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot configure container:")
|
||||
printMessageError("cannot configure container:", err)
|
||||
}
|
||||
|
||||
if err := seccomp.Load(
|
||||
|
||||
@@ -27,27 +27,27 @@ type multiStore struct {
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *multiStore) Do(aid int, f func(c Cursor)) (bool, error) {
|
||||
func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
// load or initialise new backend
|
||||
b := new(multiBackend)
|
||||
b.lock.Lock()
|
||||
if v, ok := s.backends.LoadOrStore(aid, b); ok {
|
||||
if v, ok := s.backends.LoadOrStore(identity, b); ok {
|
||||
b = v.(*multiBackend)
|
||||
} else {
|
||||
b.path = path.Join(s.base, strconv.Itoa(aid))
|
||||
b.path = path.Join(s.base, strconv.Itoa(identity))
|
||||
|
||||
// ensure directory
|
||||
if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
s.backends.CompareAndDelete(aid, b)
|
||||
s.backends.CompareAndDelete(identity, b)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// open locker file
|
||||
if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
|
||||
s.backends.CompareAndDelete(aid, b)
|
||||
s.backends.CompareAndDelete(identity, b)
|
||||
return false, err
|
||||
} else {
|
||||
b.lockfile = l
|
||||
|
||||
@@ -17,7 +17,7 @@ type Store interface {
|
||||
// Do calls f exactly once and ensures store exclusivity until f returns.
|
||||
// Returns whether f is called and any errors during the locking process.
|
||||
// Cursor provided to f becomes invalid as soon as f returns.
|
||||
Do(aid int, f func(c Cursor)) (ok bool, err error)
|
||||
Do(identity int, f func(c Cursor)) (ok bool, err error)
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -2,12 +2,9 @@ package app
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"hakurei.app/internal/app/state"
|
||||
)
|
||||
|
||||
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()} }
|
||||
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
|
||||
|
||||
// stringPair stores a value and its string representation.
|
||||
type stringPair[T comparable] struct {
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
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)
|
||||
}
|
||||
+3
-55
@@ -2,69 +2,17 @@
|
||||
package hlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container"
|
||||
)
|
||||
|
||||
const (
|
||||
bufSize = 4 * 1024
|
||||
bufSizeMax = 16 * 1024 * 1024
|
||||
)
|
||||
|
||||
var o = &suspendable{w: os.Stderr}
|
||||
var o = &container.Suspendable{Downstream: os.Stderr}
|
||||
|
||||
// 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) }
|
||||
|
||||
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 Resume() bool {
|
||||
resumed, dropped, _, err := o.Resume()
|
||||
|
||||
@@ -2,11 +2,9 @@ package hlog
|
||||
|
||||
type Output struct{}
|
||||
|
||||
func (Output) IsVerbose() bool { return Load() }
|
||||
func (Output) Verbose(v ...any) { Verbose(v...) }
|
||||
func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) }
|
||||
func (Output) WrapErr(err error, a ...any) error { return WrapErr(err, a...) }
|
||||
func (Output) PrintBaseErr(err error, fallback string) { PrintBaseError(err, fallback) }
|
||||
func (Output) Suspend() { Suspend() }
|
||||
func (Output) Resume() bool { return Resume() }
|
||||
func (Output) BeforeExit() { BeforeExit() }
|
||||
func (Output) IsVerbose() bool { return Load() }
|
||||
func (Output) Verbose(v ...any) { Verbose(v...) }
|
||||
func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) }
|
||||
func (Output) Suspend() { Suspend() }
|
||||
func (Output) Resume() bool { return Resume() }
|
||||
func (Output) BeforeExit() { BeforeExit() }
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
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
|
||||
}
|
||||
+4
-4
@@ -35,7 +35,7 @@ package
|
||||
|
||||
|
||||
*Default:*
|
||||
` <derivation hakurei-static-x86_64-unknown-linux-musl-0.2.1> `
|
||||
` <derivation hakurei-static-x86_64-unknown-linux-musl-0.2.2> `
|
||||
|
||||
|
||||
|
||||
@@ -313,7 +313,7 @@ Extra paths to make available to the container\.
|
||||
|
||||
|
||||
*Type:*
|
||||
anything
|
||||
list of attribute set of anything
|
||||
|
||||
|
||||
|
||||
@@ -723,7 +723,7 @@ Common extra paths to make available to the container\.
|
||||
|
||||
|
||||
*Type:*
|
||||
anything
|
||||
list of attribute set of anything
|
||||
|
||||
|
||||
|
||||
@@ -759,7 +759,7 @@ package
|
||||
|
||||
|
||||
*Default:*
|
||||
` <derivation hakurei-hsu-0.2.1> `
|
||||
` <derivation hakurei-hsu-0.2.2> `
|
||||
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -203,7 +203,7 @@ in
|
||||
};
|
||||
|
||||
extraPaths = mkOption {
|
||||
type = anything;
|
||||
type = listOf (attrsOf anything);
|
||||
default = [ ];
|
||||
description = ''
|
||||
Extra paths to make available to the container.
|
||||
@@ -261,7 +261,7 @@ in
|
||||
};
|
||||
|
||||
commonPaths = mkOption {
|
||||
type = types.anything;
|
||||
type = types.listOf (types.attrsOf types.anything);
|
||||
default = [ ];
|
||||
description = ''
|
||||
Common extra paths to make available to the container.
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@
|
||||
|
||||
buildGoModule rec {
|
||||
pname = "hakurei";
|
||||
version = "0.2.1";
|
||||
version = "0.2.2";
|
||||
|
||||
srcFiltered = builtins.path {
|
||||
name = "${pname}-src";
|
||||
|
||||
+12
-12
@@ -9,33 +9,33 @@ import (
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
// UpdatePerm appends [ACLUpdateOp] to [I] with the [Process] criteria.
|
||||
// UpdatePerm calls UpdatePermType with the [Process] criteria.
|
||||
func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I {
|
||||
sys.UpdatePermType(Process, path, perms...)
|
||||
return sys
|
||||
}
|
||||
|
||||
// UpdatePermType appends [ACLUpdateOp] to [I].
|
||||
// UpdatePermType maintains [acl.Perms] on a file until its [Enablement] is no longer satisfied.
|
||||
func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I {
|
||||
sys.ops = append(sys.ops, &ACLUpdateOp{et, path, perms})
|
||||
sys.ops = append(sys.ops, &aclUpdateOp{et, path, perms})
|
||||
return sys
|
||||
}
|
||||
|
||||
// ACLUpdateOp maintains [acl.Perms] on a file until its [Enablement] is no longer satisfied.
|
||||
type ACLUpdateOp struct {
|
||||
// aclUpdateOp implements [I.UpdatePermType].
|
||||
type aclUpdateOp struct {
|
||||
et Enablement
|
||||
path string
|
||||
perms acl.Perms
|
||||
}
|
||||
|
||||
func (a *ACLUpdateOp) Type() Enablement { return a.et }
|
||||
func (a *aclUpdateOp) Type() Enablement { return a.et }
|
||||
|
||||
func (a *ACLUpdateOp) apply(sys *I) error {
|
||||
func (a *aclUpdateOp) apply(sys *I) error {
|
||||
sys.verbose("applying ACL", a)
|
||||
return newOpError("acl", sys.aclUpdate(a.path, sys.uid, a.perms...), false)
|
||||
}
|
||||
|
||||
func (a *ACLUpdateOp) revert(sys *I, ec *Criteria) error {
|
||||
func (a *aclUpdateOp) revert(sys *I, ec *Criteria) error {
|
||||
if ec.hasType(a.Type()) {
|
||||
sys.verbose("stripping ACL", a)
|
||||
err := sys.aclUpdate(a.path, sys.uid)
|
||||
@@ -51,17 +51,17 @@ func (a *ACLUpdateOp) revert(sys *I, ec *Criteria) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ACLUpdateOp) Is(o Op) bool {
|
||||
target, ok := o.(*ACLUpdateOp)
|
||||
func (a *aclUpdateOp) Is(o Op) bool {
|
||||
target, ok := o.(*aclUpdateOp)
|
||||
return ok && a != nil && target != nil &&
|
||||
a.et == target.et &&
|
||||
a.path == target.path &&
|
||||
slices.Equal(a.perms, target.perms)
|
||||
}
|
||||
|
||||
func (a *ACLUpdateOp) Path() string { return a.path }
|
||||
func (a *aclUpdateOp) Path() string { return a.path }
|
||||
|
||||
func (a *ACLUpdateOp) String() string {
|
||||
func (a *aclUpdateOp) String() string {
|
||||
return fmt.Sprintf("%s type: %s path: %q",
|
||||
a.perms, TypeString(a.et), a.path)
|
||||
}
|
||||
|
||||
+49
-50
@@ -12,128 +12,127 @@ import (
|
||||
func TestACLUpdateOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"apply aclUpdate", 0xdeadbeef, 0xff,
|
||||
&ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(1)),
|
||||
}, &OpError{Op: "acl", Err: stub.UniqueError(1)}, nil, nil},
|
||||
|
||||
{"revert aclUpdate", 0xdeadbeef, 0xff,
|
||||
&ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, ([]acl.Perm)(nil)}, nil, stub.UniqueError(0)),
|
||||
}, &OpError{Op: "acl", Err: stub.UniqueError(0), Revert: true}},
|
||||
|
||||
{"success revert skip", 0xdeadbeef, Process,
|
||||
&ACLUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &ACLUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
&aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"skipping ACL", &ACLUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"skipping ACL", &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
{"success revert aclUpdate ENOENT", 0xdeadbeef, 0xff,
|
||||
&ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, ([]acl.Perm)(nil)}, nil, &os.PathError{Op: "acl_get_file", Path: "/proc/nonexistent", Err: syscall.ENOENT}),
|
||||
call("verbosef", stub.ExpectArgs{"target of ACL %s no longer exists", []any{&ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"target of ACL %s no longer exists", []any{&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
{"success", 0xdeadbeef, 0xff,
|
||||
&ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
|
||||
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, ([]acl.Perm)(nil)}, nil, nil),
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, "UpdatePerm", []opsBuilderTestCase{
|
||||
checkOpsBuilder(t, "UpdatePermType", []opsBuilderTestCase{
|
||||
{"simple",
|
||||
0xdeadbeef,
|
||||
func(sys *I) {
|
||||
func(_ *testing.T, sys *I) {
|
||||
sys.
|
||||
UpdatePerm("/run/user/1971/hakurei", acl.Execute).
|
||||
UpdatePerm("/tmp/hakurei.0/tmpdir/150", acl.Read, acl.Write, acl.Execute)
|
||||
}, []Op{
|
||||
&ACLUpdateOp{Process, "/run/user/1971/hakurei", []acl.Perm{acl.Execute}},
|
||||
&ACLUpdateOp{Process, "/tmp/hakurei.0/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
|
||||
&aclUpdateOp{Process, "/run/user/1971/hakurei", []acl.Perm{acl.Execute}},
|
||||
&aclUpdateOp{Process, "/tmp/hakurei.0/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
|
||||
}, stub.Expect{}},
|
||||
})
|
||||
checkOpsBuilder(t, "UpdatePermType", []opsBuilderTestCase{
|
||||
{"tmpdirp", 0xdeadbeef, func(sys *I) {
|
||||
|
||||
{"tmpdirp", 0xdeadbeef, func(_ *testing.T, sys *I) {
|
||||
sys.UpdatePermType(User, "/tmp/hakurei.0/tmpdir", acl.Execute)
|
||||
}, []Op{
|
||||
&ACLUpdateOp{User, "/tmp/hakurei.0/tmpdir", []acl.Perm{acl.Execute}},
|
||||
&aclUpdateOp{User, "/tmp/hakurei.0/tmpdir", []acl.Perm{acl.Execute}},
|
||||
}, stub.Expect{}},
|
||||
|
||||
{"tmpdir", 0xdeadbeef, func(sys *I) {
|
||||
{"tmpdir", 0xdeadbeef, func(_ *testing.T, sys *I) {
|
||||
sys.UpdatePermType(User, "/tmp/hakurei.0/tmpdir/150", acl.Read, acl.Write, acl.Execute)
|
||||
}, []Op{
|
||||
&ACLUpdateOp{User, "/tmp/hakurei.0/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
|
||||
&aclUpdateOp{User, "/tmp/hakurei.0/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
|
||||
}, stub.Expect{}},
|
||||
|
||||
{"share", 0xdeadbeef, func(sys *I) {
|
||||
{"share", 0xdeadbeef, func(_ *testing.T, sys *I) {
|
||||
sys.UpdatePermType(Process, "/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5", acl.Execute)
|
||||
}, []Op{
|
||||
&ACLUpdateOp{Process, "/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5", []acl.Perm{acl.Execute}},
|
||||
&aclUpdateOp{Process, "/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5", []acl.Perm{acl.Execute}},
|
||||
}, stub.Expect{}},
|
||||
|
||||
{"passwd", 0xdeadbeef, func(sys *I) {
|
||||
{"passwd", 0xdeadbeef, func(_ *testing.T, sys *I) {
|
||||
sys.
|
||||
UpdatePermType(Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd", acl.Read).
|
||||
UpdatePermType(Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group", acl.Read)
|
||||
}, []Op{
|
||||
&ACLUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd", []acl.Perm{acl.Read}},
|
||||
&ACLUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group", []acl.Perm{acl.Read}},
|
||||
&aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd", []acl.Perm{acl.Read}},
|
||||
&aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group", []acl.Perm{acl.Read}},
|
||||
}, stub.Expect{}},
|
||||
|
||||
{"wayland", 0xdeadbeef, func(sys *I) {
|
||||
{"wayland", 0xdeadbeef, func(_ *testing.T, sys *I) {
|
||||
sys.UpdatePermType(EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute)
|
||||
}, []Op{
|
||||
&ACLUpdateOp{EWayland, "/run/user/1971/wayland-0", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
|
||||
&aclUpdateOp{EWayland, "/run/user/1971/wayland-0", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
|
||||
}, stub.Expect{}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"nil", (*ACLUpdateOp)(nil), (*ACLUpdateOp)(nil), false},
|
||||
{"zero", new(ACLUpdateOp), new(ACLUpdateOp), true},
|
||||
{"nil", (*aclUpdateOp)(nil), (*aclUpdateOp)(nil), false},
|
||||
{"zero", new(aclUpdateOp), new(aclUpdateOp), true},
|
||||
|
||||
{"et differs",
|
||||
&ACLUpdateOp{
|
||||
&aclUpdateOp{
|
||||
EWayland, "/run/user/1971/wayland-0",
|
||||
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
|
||||
}, &ACLUpdateOp{
|
||||
}, &aclUpdateOp{
|
||||
EX11, "/run/user/1971/wayland-0",
|
||||
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
|
||||
}, false},
|
||||
|
||||
{"path differs", &ACLUpdateOp{
|
||||
{"path differs", &aclUpdateOp{
|
||||
EWayland, "/run/user/1971/wayland-0",
|
||||
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
|
||||
}, &ACLUpdateOp{
|
||||
}, &aclUpdateOp{
|
||||
EWayland, "/run/user/1971/wayland-1",
|
||||
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
|
||||
}, false},
|
||||
|
||||
{"perms differs", &ACLUpdateOp{
|
||||
{"perms differs", &aclUpdateOp{
|
||||
EWayland, "/run/user/1971/wayland-0",
|
||||
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
|
||||
}, &ACLUpdateOp{
|
||||
}, &aclUpdateOp{
|
||||
EWayland, "/run/user/1971/wayland-0",
|
||||
[]acl.Perm{acl.Read, acl.Write},
|
||||
}, false},
|
||||
|
||||
{"equals", &ACLUpdateOp{
|
||||
{"equals", &aclUpdateOp{
|
||||
EWayland, "/run/user/1971/wayland-0",
|
||||
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
|
||||
}, &ACLUpdateOp{
|
||||
}, &aclUpdateOp{
|
||||
EWayland, "/run/user/1971/wayland-0",
|
||||
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
|
||||
}, true},
|
||||
@@ -141,42 +140,42 @@ func TestACLUpdateOp(t *testing.T) {
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"clear",
|
||||
&ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{}},
|
||||
&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{}},
|
||||
Process, "/proc/nonexistent",
|
||||
`--- type: process path: "/proc/nonexistent"`},
|
||||
|
||||
{"read",
|
||||
&ACLUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0", []acl.Perm{acl.Read}},
|
||||
&aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0", []acl.Perm{acl.Read}},
|
||||
User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0",
|
||||
`r-- type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0"`},
|
||||
|
||||
{"write",
|
||||
&ACLUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1", []acl.Perm{acl.Write}},
|
||||
&aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1", []acl.Perm{acl.Write}},
|
||||
User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1",
|
||||
`-w- type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1"`},
|
||||
|
||||
{"execute",
|
||||
&ACLUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2", []acl.Perm{acl.Execute}},
|
||||
&aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2", []acl.Perm{acl.Execute}},
|
||||
User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2",
|
||||
`--x type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2"`},
|
||||
|
||||
{"wayland",
|
||||
&ACLUpdateOp{EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland", []acl.Perm{acl.Read, acl.Write}},
|
||||
&aclUpdateOp{EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland", []acl.Perm{acl.Read, acl.Write}},
|
||||
EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland",
|
||||
`rw- type: wayland path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland"`},
|
||||
|
||||
{"x11",
|
||||
&ACLUpdateOp{EX11, "/tmp/.X11-unix/X0", []acl.Perm{acl.Read, acl.Execute}},
|
||||
&aclUpdateOp{EX11, "/tmp/.X11-unix/X0", []acl.Perm{acl.Read, acl.Execute}},
|
||||
EX11, "/tmp/.X11-unix/X0",
|
||||
`r-x type: x11 path: "/tmp/.X11-unix/X0"`},
|
||||
|
||||
{"dbus",
|
||||
&ACLUpdateOp{EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus", []acl.Perm{acl.Write, acl.Execute}},
|
||||
&aclUpdateOp{EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus", []acl.Perm{acl.Write, acl.Execute}},
|
||||
EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus",
|
||||
`-wx type: dbus path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus"`},
|
||||
|
||||
{"pulseaudio",
|
||||
&ACLUpdateOp{EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
|
||||
&aclUpdateOp{EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
|
||||
EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse",
|
||||
`rwx type: pulseaudio path: "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse"`},
|
||||
})
|
||||
|
||||
+64
-37
@@ -28,9 +28,10 @@ func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyDBus finalises configuration and appends [DBusProxyOp] to [I].
|
||||
// ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via [dbus] and terminates it on revert.
|
||||
// This [Op] is always [Process] scoped.
|
||||
func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) (func(), error) {
|
||||
d := new(DBusProxyOp)
|
||||
d := new(dbusProxyOp)
|
||||
|
||||
// session bus is required as otherwise this is effectively a very expensive noop
|
||||
if session == nil {
|
||||
@@ -42,10 +43,10 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
|
||||
d.system = system != nil
|
||||
|
||||
var sessionBus, systemBus dbus.ProxyPair
|
||||
sessionBus[0], systemBus[0] = dbus.Address()
|
||||
sessionBus[0], systemBus[0] = sys.dbusAddress()
|
||||
sessionBus[1], systemBus[1] = sessionPath, systemPath
|
||||
d.out = &linePrefixWriter{println: log.Println, prefix: "(dbus) ", msg: new(strings.Builder)}
|
||||
if final, err := dbus.Finalise(sessionBus, systemBus, session, system); err != nil {
|
||||
d.out = &linePrefixWriter{println: log.Println, prefix: "(dbus) ", buf: new(strings.Builder)}
|
||||
if final, err := sys.dbusFinalise(sessionBus, systemBus, session, system); err != nil {
|
||||
if errors.Is(err, syscall.EINVAL) {
|
||||
return nil, newOpErrorMessage("dbus", err,
|
||||
"message bus proxy configuration contains NUL byte", false)
|
||||
@@ -53,14 +54,14 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
|
||||
return nil, newOpErrorMessage("dbus", err,
|
||||
fmt.Sprintf("cannot finalise message bus proxy: %v", err), false)
|
||||
} else {
|
||||
if msg.IsVerbose() {
|
||||
msg.Verbose("session bus proxy:", session.Args(sessionBus))
|
||||
if sys.isVerbose() {
|
||||
sys.verbose("session bus proxy:", session.Args(sessionBus))
|
||||
if system != nil {
|
||||
msg.Verbose("system bus proxy:", system.Args(systemBus))
|
||||
sys.verbose("system bus proxy:", system.Args(systemBus))
|
||||
}
|
||||
|
||||
// this calls the argsWt String method
|
||||
msg.Verbose("message bus proxy final args:", final.WriterTo)
|
||||
sys.verbose("message bus proxy final args:", final.WriterTo)
|
||||
}
|
||||
|
||||
d.final = final
|
||||
@@ -70,9 +71,8 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
|
||||
return d.out.Dump, nil
|
||||
}
|
||||
|
||||
// DBusProxyOp starts xdg-dbus-proxy via [dbus] and terminates it on revert.
|
||||
// This [Op] is always [Process] scoped.
|
||||
type DBusProxyOp struct {
|
||||
// dbusProxyOp implements [I.ProxyDBus].
|
||||
type dbusProxyOp struct {
|
||||
proxy *dbus.Proxy // populated during apply
|
||||
|
||||
final *dbus.Final
|
||||
@@ -81,40 +81,43 @@ type DBusProxyOp struct {
|
||||
system bool
|
||||
}
|
||||
|
||||
func (d *DBusProxyOp) Type() Enablement { return Process }
|
||||
func (d *dbusProxyOp) Type() Enablement { return Process }
|
||||
|
||||
func (d *DBusProxyOp) apply(sys *I) error {
|
||||
msg.Verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0])
|
||||
func (d *dbusProxyOp) apply(sys *I) error {
|
||||
sys.verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0])
|
||||
if d.system {
|
||||
msg.Verbosef("system bus proxy on %q for upstream %q", d.final.System[1], d.final.System[0])
|
||||
sys.verbosef("system bus proxy on %q for upstream %q", d.final.System[1], d.final.System[0])
|
||||
}
|
||||
|
||||
d.proxy = dbus.New(sys.ctx, d.final, d.out)
|
||||
if err := d.proxy.Start(); err != nil {
|
||||
if err := sys.dbusProxyStart(d.proxy); err != nil {
|
||||
d.out.Dump()
|
||||
return newOpErrorMessage("dbus", err,
|
||||
fmt.Sprintf("cannot start message bus proxy: %v", err), false)
|
||||
}
|
||||
msg.Verbose("starting message bus proxy", d.proxy)
|
||||
sys.verbose("starting message bus proxy", d.proxy)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DBusProxyOp) revert(*I, *Criteria) error {
|
||||
func (d *dbusProxyOp) revert(sys *I, _ *Criteria) error {
|
||||
// criteria ignored here since dbus is always process-scoped
|
||||
msg.Verbose("terminating message bus proxy")
|
||||
d.proxy.Close()
|
||||
defer msg.Verbose("message bus proxy exit")
|
||||
err := d.proxy.Wait()
|
||||
sys.verbose("terminating message bus proxy")
|
||||
sys.dbusProxyClose(d.proxy)
|
||||
|
||||
exitMessage := "message bus proxy exit"
|
||||
defer func() { sys.verbose(exitMessage) }()
|
||||
|
||||
err := sys.dbusProxyWait(d.proxy)
|
||||
if errors.Is(err, context.Canceled) {
|
||||
msg.Verbose("message bus proxy canceled upstream")
|
||||
exitMessage = "message bus proxy canceled upstream"
|
||||
err = nil
|
||||
}
|
||||
return newOpErrorMessage("dbus", err,
|
||||
fmt.Sprintf("message bus proxy error: %v", err), true)
|
||||
}
|
||||
|
||||
func (d *DBusProxyOp) Is(o Op) bool {
|
||||
target, ok := o.(*DBusProxyOp)
|
||||
func (d *dbusProxyOp) Is(o Op) bool {
|
||||
target, ok := o.(*dbusProxyOp)
|
||||
return ok && d != nil && target != nil &&
|
||||
d.system == target.system &&
|
||||
d.final != nil && target.final != nil &&
|
||||
@@ -125,15 +128,23 @@ func (d *DBusProxyOp) Is(o Op) bool {
|
||||
reflect.DeepEqual(d.final.WriterTo, target.final.WriterTo)
|
||||
}
|
||||
|
||||
func (d *DBusProxyOp) Path() string { return container.Nonexistent }
|
||||
func (d *DBusProxyOp) String() string { return d.proxy.String() }
|
||||
func (d *dbusProxyOp) Path() string { return container.Nonexistent }
|
||||
func (d *dbusProxyOp) String() string { return d.proxy.String() }
|
||||
|
||||
const (
|
||||
// lpwSizeThreshold is the threshold of bytes written to linePrefixWriter which,
|
||||
// if reached or exceeded, causes linePrefixWriter to drop all future writes.
|
||||
lpwSizeThreshold = 1 << 24
|
||||
)
|
||||
|
||||
// linePrefixWriter calls println with a prefix for every line written.
|
||||
type linePrefixWriter struct {
|
||||
prefix string
|
||||
println func(v ...any)
|
||||
msg *strings.Builder
|
||||
msgbuf []string
|
||||
|
||||
n int
|
||||
msg []string
|
||||
buf *strings.Builder
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
@@ -145,29 +156,45 @@ func (s *linePrefixWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (s *linePrefixWriter) write(p []byte, a int) (int, error) {
|
||||
if s.n >= lpwSizeThreshold {
|
||||
if len(p) == 0 {
|
||||
return a, nil
|
||||
}
|
||||
return a, syscall.ENOMEM
|
||||
}
|
||||
|
||||
if i := bytes.IndexByte(p, '\n'); i == -1 {
|
||||
n, _ := s.msg.Write(p)
|
||||
n, _ := s.buf.Write(p)
|
||||
s.n += n
|
||||
return a + n, nil
|
||||
} else {
|
||||
n, _ := s.msg.Write(p[:i])
|
||||
n, _ := s.buf.Write(p[:i])
|
||||
s.n += n + 1
|
||||
|
||||
// allow container init messages through
|
||||
v := s.msg.String()
|
||||
v := s.buf.String()
|
||||
if strings.HasPrefix(v, "init: ") {
|
||||
s.n -= len(v) + 1
|
||||
// pass through container init messages
|
||||
s.println(s.prefix + v)
|
||||
} else {
|
||||
s.msgbuf = append(s.msgbuf, v)
|
||||
s.msg = append(s.msg, v)
|
||||
}
|
||||
|
||||
s.msg.Reset()
|
||||
s.buf.Reset()
|
||||
return s.write(p[i+1:], a+n+1)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *linePrefixWriter) Dump() {
|
||||
s.mu.RLock()
|
||||
for _, m := range s.msgbuf {
|
||||
for _, m := range s.msg {
|
||||
s.println(s.prefix + m)
|
||||
}
|
||||
if s.buf != nil && s.buf.Len() != 0 {
|
||||
s.println("*" + s.prefix + s.buf.String())
|
||||
}
|
||||
if s.n >= lpwSizeThreshold {
|
||||
s.println("+" + s.prefix + "write threshold reached, output may be incomplete")
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,659 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func TestDBusProxyOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"dbusProxyStart", 0xdeadbeef, 0xff, &dbusProxyOp{
|
||||
final: dbusNewFinalSample(4),
|
||||
out: new(linePrefixWriter), // panics on write
|
||||
system: true,
|
||||
}, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"session bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "unix:path=/run/user/1000/bus"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"system bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "unix:path=/run/dbus/system_bus_socket"}}, nil, nil),
|
||||
call("dbusProxyStart", stub.ExpectArgs{dbusNewFinalSample(4)}, nil, stub.UniqueError(2)),
|
||||
}, &OpError{
|
||||
Op: "dbus", Err: stub.UniqueError(2),
|
||||
Msg: "cannot start message bus proxy: unique error 2 injected by the test suite",
|
||||
}, nil, nil},
|
||||
|
||||
{"dbusProxyWait", 0xdeadbeef, 0xff, &dbusProxyOp{
|
||||
final: dbusNewFinalSample(3),
|
||||
}, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"session bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "unix:path=/run/user/1000/bus"}}, nil, nil),
|
||||
call("dbusProxyStart", stub.ExpectArgs{dbusNewFinalSample(3)}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"starting message bus proxy", ignoreValue{}}}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"terminating message bus proxy"}}, nil, nil),
|
||||
call("dbusProxyClose", stub.ExpectArgs{dbusNewFinalSample(3)}, nil, nil),
|
||||
call("dbusProxyWait", stub.ExpectArgs{dbusNewFinalSample(3)}, nil, stub.UniqueError(1)),
|
||||
call("verbose", stub.ExpectArgs{[]any{"message bus proxy exit"}}, nil, nil),
|
||||
}, &OpError{
|
||||
Op: "dbus", Err: stub.UniqueError(1), Revert: true,
|
||||
Msg: "message bus proxy error: unique error 1 injected by the test suite",
|
||||
}},
|
||||
|
||||
{"success dbusProxyWait cancel", 0xdeadbeef, 0xff, &dbusProxyOp{
|
||||
final: dbusNewFinalSample(2),
|
||||
system: true,
|
||||
}, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"session bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "unix:path=/run/user/1000/bus"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"system bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "unix:path=/run/dbus/system_bus_socket"}}, nil, nil),
|
||||
call("dbusProxyStart", stub.ExpectArgs{dbusNewFinalSample(2)}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"starting message bus proxy", ignoreValue{}}}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"terminating message bus proxy"}}, nil, nil),
|
||||
call("dbusProxyClose", stub.ExpectArgs{dbusNewFinalSample(2)}, nil, nil),
|
||||
call("dbusProxyWait", stub.ExpectArgs{dbusNewFinalSample(2)}, nil, context.Canceled),
|
||||
call("verbose", stub.ExpectArgs{[]any{"message bus proxy canceled upstream"}}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
{"success", 0xdeadbeef, 0xff, &dbusProxyOp{
|
||||
final: dbusNewFinalSample(1),
|
||||
system: true,
|
||||
}, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"session bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "unix:path=/run/user/1000/bus"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"system bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "unix:path=/run/dbus/system_bus_socket"}}, nil, nil),
|
||||
call("dbusProxyStart", stub.ExpectArgs{dbusNewFinalSample(1)}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"starting message bus proxy", ignoreValue{}}}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"terminating message bus proxy"}}, nil, nil),
|
||||
call("dbusProxyClose", stub.ExpectArgs{dbusNewFinalSample(1)}, nil, nil),
|
||||
call("dbusProxyWait", stub.ExpectArgs{dbusNewFinalSample(1)}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"message bus proxy exit"}}, nil, nil),
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, "ProxyDBus", []opsBuilderTestCase{
|
||||
{"nil session", 0xcafebabe, func(t *testing.T, sys *I) {
|
||||
wantErr := &OpError{
|
||||
Op: "dbus", Err: ErrDBusConfig,
|
||||
Msg: "attempted to create message bus proxy args without session bus config",
|
||||
}
|
||||
if f, err := sys.ProxyDBus(nil, new(dbus.Config), "", ""); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Errorf("ProxyDBus: error = %v, want %v", err, wantErr)
|
||||
} else if f != nil {
|
||||
t.Errorf("ProxyDBus: f = %p", f)
|
||||
}
|
||||
}, nil, stub.Expect{}},
|
||||
|
||||
{"dbusFinalise NUL", 0xcafebabe, func(_ *testing.T, sys *I) {
|
||||
defer func() {
|
||||
want := "message bus proxy configuration contains NUL byte"
|
||||
if r := recover(); r != want {
|
||||
t.Errorf("MustProxyDBus: panic = %v, want %v", r, want)
|
||||
}
|
||||
}()
|
||||
|
||||
sys.MustProxyDBus(
|
||||
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", &dbus.Config{
|
||||
// use impossible value here as an implicit assert that it goes through the stub
|
||||
Talk: []string{"session\x00"}, Filter: true,
|
||||
}, "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", &dbus.Config{
|
||||
// use impossible value here as an implicit assert that it goes through the stub
|
||||
Talk: []string{"system\x00"}, Filter: true,
|
||||
})
|
||||
}, nil, stub.Expect{Calls: []stub.Call{
|
||||
call("dbusAddress", stub.ExpectArgs{}, [2]string{"unix:path=/run/user/1000/bus", "unix:path=/run/dbus/system_bus_socket"}, nil),
|
||||
call("dbusFinalise", stub.ExpectArgs{
|
||||
dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"},
|
||||
dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"},
|
||||
&dbus.Config{Talk: []string{"session\x00"}, Filter: true},
|
||||
&dbus.Config{Talk: []string{"system\x00"}, Filter: true},
|
||||
}, (*dbus.Final)(nil), syscall.EINVAL),
|
||||
}}},
|
||||
|
||||
{"dbusFinalise", 0xcafebabe, func(_ *testing.T, sys *I) {
|
||||
wantErr := &OpError{
|
||||
Op: "dbus", Err: stub.UniqueError(0),
|
||||
Msg: "cannot finalise message bus proxy: unique error 0 injected by the test suite",
|
||||
}
|
||||
if f, err := sys.ProxyDBus(
|
||||
&dbus.Config{
|
||||
// use impossible value here as an implicit assert that it goes through the stub
|
||||
Talk: []string{"session\x00"}, Filter: true,
|
||||
}, &dbus.Config{
|
||||
// use impossible value here as an implicit assert that it goes through the stub
|
||||
Talk: []string{"system\x00"}, Filter: true,
|
||||
},
|
||||
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus",
|
||||
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Errorf("ProxyDBus: error = %v", err)
|
||||
} else if f != nil {
|
||||
t.Errorf("ProxyDBus: f = %p", f)
|
||||
}
|
||||
}, nil, stub.Expect{Calls: []stub.Call{
|
||||
call("dbusAddress", stub.ExpectArgs{}, [2]string{"unix:path=/run/user/1000/bus", "unix:path=/run/dbus/system_bus_socket"}, nil),
|
||||
call("dbusFinalise", stub.ExpectArgs{
|
||||
dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"},
|
||||
dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"},
|
||||
&dbus.Config{Talk: []string{"session\x00"}, Filter: true},
|
||||
&dbus.Config{Talk: []string{"system\x00"}, Filter: true},
|
||||
}, (*dbus.Final)(nil), stub.UniqueError(0)),
|
||||
}}},
|
||||
|
||||
{"full", 0xcafebabe, func(_ *testing.T, sys *I) {
|
||||
sys.MustProxyDBus(
|
||||
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", &dbus.Config{
|
||||
// use impossible value here as an implicit assert that it goes through the stub
|
||||
Talk: []string{"session\x00"}, Filter: true,
|
||||
}, "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", &dbus.Config{
|
||||
// use impossible value here as an implicit assert that it goes through the stub
|
||||
Talk: []string{"system\x00"}, Filter: true,
|
||||
})
|
||||
}, []Op{
|
||||
&dbusProxyOp{
|
||||
final: dbusNewFinalSample(0),
|
||||
system: true,
|
||||
},
|
||||
}, stub.Expect{Calls: []stub.Call{
|
||||
call("dbusAddress", stub.ExpectArgs{}, [2]string{"unix:path=/run/user/1000/bus", "unix:path=/run/dbus/system_bus_socket"}, nil),
|
||||
call("dbusFinalise", stub.ExpectArgs{
|
||||
dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"},
|
||||
dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"},
|
||||
&dbus.Config{Talk: []string{"session\x00"}, Filter: true},
|
||||
&dbus.Config{Talk: []string{"system\x00"}, Filter: true},
|
||||
}, dbusNewFinalSample(0), nil),
|
||||
call("isVerbose", stub.ExpectArgs{}, true, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "--filter", "--talk=session\x00"}}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"system bus proxy:", []string{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "--filter", "--talk=system\x00"}}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs([]string{"unique", "value", "0", "injected", "by", "the", "test", "suite"})}}, nil, nil),
|
||||
}}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"nil", (*dbusProxyOp)(nil), (*dbusProxyOp)(nil), false},
|
||||
{"zero", new(dbusProxyOp), new(dbusProxyOp), false},
|
||||
|
||||
{"system differs", &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: false,
|
||||
}, &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, false},
|
||||
|
||||
{"wt differs", &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1001/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, false},
|
||||
|
||||
{"final system upstream differs", &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket\x00"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, false},
|
||||
|
||||
{"final session upstream differs", &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1001/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, false},
|
||||
|
||||
{"final system differs", &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.1/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, false},
|
||||
|
||||
{"final session differs", &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1001/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, false},
|
||||
|
||||
{"equals", &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, &dbusProxyOp{final: &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{
|
||||
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
|
||||
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
|
||||
}),
|
||||
}, system: true,
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"dbus", new(dbusProxyOp),
|
||||
Process, "/proc/nonexistent",
|
||||
"(invalid dbus proxy)"},
|
||||
})
|
||||
}
|
||||
|
||||
func dbusNewFinalSample(v int) *dbus.Final {
|
||||
return &dbus.Final{
|
||||
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"},
|
||||
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"},
|
||||
|
||||
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
|
||||
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/dbus/system_bus_socket"}}}},
|
||||
|
||||
WriterTo: helper.MustNewCheckedArgs([]string{"unique", "value", strconv.Itoa(v), "injected", "by", "the", "test", "suite"}),
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinePrefixWriter(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
prefix string
|
||||
f func(w func(s string))
|
||||
wantErr []error
|
||||
wantPt []string
|
||||
want []string
|
||||
wantExt []string
|
||||
wantBuf string
|
||||
}{
|
||||
{"nop", "(nop) ", func(func(string)) {}, nil, nil, nil, nil, ""},
|
||||
|
||||
{"partial", "(partial) ", func(w func(string)) {
|
||||
w("C-65533: -> ")
|
||||
}, nil, nil, nil, []string{
|
||||
"*(partial) C-65533: -> ",
|
||||
}, "C-65533: -> "},
|
||||
|
||||
{"break", "(break) ", func(w func(string)) {
|
||||
w("C-65533: -> ")
|
||||
w("org.freedesktop.DBus fake ListNames\n")
|
||||
}, nil, nil, []string{
|
||||
"C-65533: -> org.freedesktop.DBus fake ListNames",
|
||||
}, nil, ""},
|
||||
|
||||
{"break pt", "(break pt) ", func(w func(string)) {
|
||||
w("init: ")
|
||||
w("received setup parameters\n")
|
||||
}, nil, []string{
|
||||
"init: received setup parameters",
|
||||
}, nil, nil, ""},
|
||||
|
||||
{"threshold", "(threshold) ", func(w func(s string)) {
|
||||
w(string(make([]byte, lpwSizeThreshold)))
|
||||
w("\n")
|
||||
}, []error{nil, syscall.ENOMEM}, nil, nil, []string{
|
||||
"*(threshold) " + string(make([]byte, lpwSizeThreshold)),
|
||||
"+(threshold) write threshold reached, output may be incomplete",
|
||||
}, string(make([]byte, lpwSizeThreshold))},
|
||||
|
||||
{"threshold multi", "(threshold multi) ", func(w func(s string)) {
|
||||
w(":3\n")
|
||||
w(string(make([]byte, lpwSizeThreshold-3)))
|
||||
w("\n")
|
||||
}, []error{nil, nil, syscall.ENOMEM}, nil, []string{
|
||||
":3",
|
||||
}, []string{
|
||||
"*(threshold multi) " + string(make([]byte, lpwSizeThreshold-3)),
|
||||
"+(threshold multi) write threshold reached, output may be incomplete",
|
||||
}, string(make([]byte, lpwSizeThreshold-3))},
|
||||
|
||||
{"threshold multi partial", "(threshold multi partial) ", func(w func(s string)) {
|
||||
w(":3\n")
|
||||
w(string(make([]byte, lpwSizeThreshold-2)))
|
||||
w("dropped\n")
|
||||
}, []error{nil, nil, syscall.ENOMEM}, nil, []string{
|
||||
":3",
|
||||
}, []string{
|
||||
"*(threshold multi partial) " + string(make([]byte, lpwSizeThreshold-2)),
|
||||
"+(threshold multi partial) write threshold reached, output may be incomplete",
|
||||
}, string(make([]byte, lpwSizeThreshold-2))},
|
||||
|
||||
{"threshold exact", "(threshold exact) ", func(w func(s string)) {
|
||||
w(string(make([]byte, lpwSizeThreshold-1)))
|
||||
w("\n")
|
||||
}, nil, nil, []string{
|
||||
string(make([]byte, lpwSizeThreshold-1)),
|
||||
}, []string{
|
||||
"+(threshold exact) write threshold reached, output may be incomplete",
|
||||
}, ""},
|
||||
|
||||
{"sample", "(dbus) ", func(w func(s string)) {
|
||||
w("init: received setup parameters\n")
|
||||
w(`init: mounting "/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" flags 0x0` + "\n")
|
||||
w(`init: resolved "/host/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" on "/sysroot/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" flags 0x4005` + "\n")
|
||||
w(`init: mounting "/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" flags 0x0` + "\n")
|
||||
w(`init: resolved "/host/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" on "/sysroot/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" flags 0x4005` + "\n")
|
||||
w(`init: mounting "/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" flags 0x0` + "\n")
|
||||
w(`init: resolved "/host/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" on "/sysroot/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" flags 0x4005` + "\n")
|
||||
w(`init: mounting "/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" flags 0x0` + "\n")
|
||||
w(`init: resolved "/host/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" on "/sysroot/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" flags 0x4005` + "\n")
|
||||
w(`init: mounting "/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" flags 0x0` + "\n")
|
||||
w(`init: resolved "/host/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" on "/sysroot/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" flags 0x4005` + "\n")
|
||||
w(`init: mounting "/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" flags 0x0` + "\n")
|
||||
w(`init: resolved "/host/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" on "/sysroot/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" flags 0x4005` + "\n")
|
||||
w(`init: mounting "/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" flags 0x0` + "\n")
|
||||
w(`init: resolved "/host/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" on "/sysroot/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" flags 0x4005` + "\n")
|
||||
w(`init: mounting "/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib64" flags 0x0` + "\n")
|
||||
w(`init: resolved "/host/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" on "/sysroot/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib64" flags 0x4005` + "\n")
|
||||
w(`init: mounting "/run/user/1000" flags 0x0` + "\n")
|
||||
w(`init: resolved "/host/run/user/1000" on "/sysroot/run/user/1000" flags 0x4005` + "\n")
|
||||
w(`init: mounting "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" flags 0x2` + "\n")
|
||||
w(`init: resolved "/host/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" on "/sysroot/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" flags 0x4004` + "\n")
|
||||
w(`init: mounting "/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" flags 0x0` + "\n")
|
||||
w(`init: resolved "/host/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" on "/sysroot/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" flags 0x4005` + "\n")
|
||||
w("init: resolving presets 0xf\n")
|
||||
w("init: 68 filter rules loaded\n")
|
||||
w("init: starting initial program /nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin/xdg-dbus-proxy\n")
|
||||
w("C1: -> org.freedesktop.DBus call org.freedesktop.DBus.Hello at /org/freedesktop/DBus\n")
|
||||
w("C-65536: -> org.freedesktop.DBus fake wildcarded AddMatch for org.freedesktop.portal\n")
|
||||
w("C-65535: -> org.freedesktop.DBus fake AddMatch for org.freedesktop.Notifications\n")
|
||||
w("C-65534: -> org.freedesktop.DBus fake GetNameOwner for org.freedesktop.Notifications\n")
|
||||
w("C-65533: -> org.freedesktop.DBus fake ListNames\n")
|
||||
w("B1: <- org.freedesktop.DBus return from C1\n")
|
||||
w("B2: <- org.freedesktop.DBus signal org.freedesktop.DBus.NameAcquired at /org/freedesktop/DBus\n")
|
||||
w("B3: <- org.freedesktop.DBus return from C-65536\n")
|
||||
w("*SKIPPED*\n")
|
||||
w("B4: <- org.freedesktop.DBus return from C-65535\n")
|
||||
w("*SKIPPED*\n")
|
||||
w("B5: <- org.freedesktop.DBus return error org.freedesktop.DBus.Error.NameHasNoOwner from C-65534\n")
|
||||
w("*SKIPPED*\n")
|
||||
w("B6: <- org.freedesktop.DBus return from C-65533\n")
|
||||
w("C-65532: -> org.freedesktop.DBus fake GetNameOwner for org.freedesktop.DBus\n")
|
||||
w("*SKIPPED*\n")
|
||||
w("B7: <- org.freedesktop.DBus return from C-65532\n")
|
||||
w("*SKIPPED*\n")
|
||||
w("C2: -> org.freedesktop.DBus call org.freedesktop.DBus.AddMatch at /org/freedesktop/DBus\n")
|
||||
w("C3: -> org.freedesktop.DBus call org.freedesktop.DBus.GetNameOwner at /org/freedesktop/DBus\n")
|
||||
w("C4: -> org.freedesktop.DBus call org.freedesktop.DBus.AddMatch at /org/freedesktop/DBus\n")
|
||||
w("C5: -> org.freedesktop.DBus call org.freedesktop.DBus.StartServiceByName at /org/freedesktop/DBus\n")
|
||||
w("B8: <- org.freedesktop.DBus return from C2\n")
|
||||
w("B9: <- org.freedesktop.DBus return error org.freedesktop.DBus.Error.NameHasNoOwner from C3\n")
|
||||
w("B10: <- org.freedesktop.DBus return from C4\n")
|
||||
w("B12: <- org.freedesktop.DBus signal org.freedesktop.DBus.NameOwnerChanged at /org/freedesktop/DBus\n")
|
||||
w("B11: <- org.freedesktop.DBus return from C5\n")
|
||||
w("C6: -> org.freedesktop.DBus call org.freedesktop.DBus.GetNameOwner at /org/freedesktop/DBus\n")
|
||||
w("B13: <- org.freedesktop.DBus return from C6\n")
|
||||
w("C7: -> :1.4 call org.freedesktop.Notifications.GetServerInformation at /org/freedesktop/Notifications\n")
|
||||
w("B4: <- :1.4 return from C7\n")
|
||||
w("C8: -> :1.4 call org.freedesktop.Notifications.GetServerInformation at /org/freedesktop/Notifications\n")
|
||||
w("B5: <- :1.4 return from C8\n")
|
||||
w("C9: -> :1.4 call org.freedesktop.Notifications.Notify at /org/freedesktop/Notifications\n")
|
||||
w("B6: <- :1.4 return from C9\n")
|
||||
w("C10: -> org.freedesktop.DBus call org.freedesktop.DBus.RemoveMatch at /org/freedesktop/DBus\n")
|
||||
w("C11: -> org.freedesktop.DBus call org.freedesktop.DBus.RemoveMatch at /org/freedesktop/DBus\n")
|
||||
w("B14: <- org.freedesktop.DBus return from C10\n")
|
||||
w("B15: <- org.freedesktop.DBus return from C11\n")
|
||||
w("init: initial process exited with code 0\n")
|
||||
}, nil, []string{
|
||||
"init: received setup parameters",
|
||||
`init: mounting "/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" flags 0x0`,
|
||||
`init: resolved "/host/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" on "/sysroot/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" flags 0x4005`,
|
||||
`init: mounting "/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" flags 0x0`,
|
||||
`init: resolved "/host/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" on "/sysroot/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" flags 0x4005`,
|
||||
`init: mounting "/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" flags 0x0`,
|
||||
`init: resolved "/host/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" on "/sysroot/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" flags 0x4005`,
|
||||
`init: mounting "/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" flags 0x0`,
|
||||
`init: resolved "/host/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" on "/sysroot/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" flags 0x4005`,
|
||||
`init: mounting "/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" flags 0x0`,
|
||||
`init: resolved "/host/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" on "/sysroot/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" flags 0x4005`,
|
||||
`init: mounting "/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" flags 0x0`,
|
||||
`init: resolved "/host/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" on "/sysroot/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" flags 0x4005`,
|
||||
`init: mounting "/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" flags 0x0`,
|
||||
`init: resolved "/host/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" on "/sysroot/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" flags 0x4005`,
|
||||
`init: mounting "/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib64" flags 0x0`,
|
||||
`init: resolved "/host/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" on "/sysroot/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib64" flags 0x4005`,
|
||||
`init: mounting "/run/user/1000" flags 0x0`,
|
||||
`init: resolved "/host/run/user/1000" on "/sysroot/run/user/1000" flags 0x4005`,
|
||||
`init: mounting "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" flags 0x2`,
|
||||
`init: resolved "/host/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" on "/sysroot/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" flags 0x4004`,
|
||||
`init: mounting "/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" flags 0x0`,
|
||||
`init: resolved "/host/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" on "/sysroot/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" flags 0x4005`,
|
||||
"init: resolving presets 0xf",
|
||||
"init: 68 filter rules loaded",
|
||||
"init: starting initial program /nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin/xdg-dbus-proxy",
|
||||
|
||||
"init: initial process exited with code 0",
|
||||
}, []string{
|
||||
"C1: -> org.freedesktop.DBus call org.freedesktop.DBus.Hello at /org/freedesktop/DBus",
|
||||
"C-65536: -> org.freedesktop.DBus fake wildcarded AddMatch for org.freedesktop.portal",
|
||||
"C-65535: -> org.freedesktop.DBus fake AddMatch for org.freedesktop.Notifications",
|
||||
"C-65534: -> org.freedesktop.DBus fake GetNameOwner for org.freedesktop.Notifications",
|
||||
"C-65533: -> org.freedesktop.DBus fake ListNames",
|
||||
"B1: <- org.freedesktop.DBus return from C1",
|
||||
"B2: <- org.freedesktop.DBus signal org.freedesktop.DBus.NameAcquired at /org/freedesktop/DBus",
|
||||
"B3: <- org.freedesktop.DBus return from C-65536",
|
||||
"*SKIPPED*",
|
||||
"B4: <- org.freedesktop.DBus return from C-65535",
|
||||
"*SKIPPED*",
|
||||
"B5: <- org.freedesktop.DBus return error org.freedesktop.DBus.Error.NameHasNoOwner from C-65534",
|
||||
"*SKIPPED*",
|
||||
"B6: <- org.freedesktop.DBus return from C-65533",
|
||||
"C-65532: -> org.freedesktop.DBus fake GetNameOwner for org.freedesktop.DBus",
|
||||
"*SKIPPED*",
|
||||
"B7: <- org.freedesktop.DBus return from C-65532",
|
||||
"*SKIPPED*",
|
||||
"C2: -> org.freedesktop.DBus call org.freedesktop.DBus.AddMatch at /org/freedesktop/DBus",
|
||||
"C3: -> org.freedesktop.DBus call org.freedesktop.DBus.GetNameOwner at /org/freedesktop/DBus",
|
||||
"C4: -> org.freedesktop.DBus call org.freedesktop.DBus.AddMatch at /org/freedesktop/DBus",
|
||||
"C5: -> org.freedesktop.DBus call org.freedesktop.DBus.StartServiceByName at /org/freedesktop/DBus",
|
||||
"B8: <- org.freedesktop.DBus return from C2",
|
||||
"B9: <- org.freedesktop.DBus return error org.freedesktop.DBus.Error.NameHasNoOwner from C3",
|
||||
"B10: <- org.freedesktop.DBus return from C4",
|
||||
"B12: <- org.freedesktop.DBus signal org.freedesktop.DBus.NameOwnerChanged at /org/freedesktop/DBus",
|
||||
"B11: <- org.freedesktop.DBus return from C5",
|
||||
"C6: -> org.freedesktop.DBus call org.freedesktop.DBus.GetNameOwner at /org/freedesktop/DBus",
|
||||
"B13: <- org.freedesktop.DBus return from C6",
|
||||
"C7: -> :1.4 call org.freedesktop.Notifications.GetServerInformation at /org/freedesktop/Notifications",
|
||||
"B4: <- :1.4 return from C7",
|
||||
"C8: -> :1.4 call org.freedesktop.Notifications.GetServerInformation at /org/freedesktop/Notifications",
|
||||
"B5: <- :1.4 return from C8",
|
||||
"C9: -> :1.4 call org.freedesktop.Notifications.Notify at /org/freedesktop/Notifications",
|
||||
"B6: <- :1.4 return from C9",
|
||||
"C10: -> org.freedesktop.DBus call org.freedesktop.DBus.RemoveMatch at /org/freedesktop/DBus",
|
||||
"C11: -> org.freedesktop.DBus call org.freedesktop.DBus.RemoveMatch at /org/freedesktop/DBus",
|
||||
"B14: <- org.freedesktop.DBus return from C10",
|
||||
"B15: <- org.freedesktop.DBus return from C11",
|
||||
}, nil, ""},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gotPt := make([]string, 0, len(tc.wantPt))
|
||||
out := &linePrefixWriter{
|
||||
prefix: tc.prefix,
|
||||
println: func(v ...any) {
|
||||
if len(v) != 1 {
|
||||
t.Fatalf("invalid call to println: %#v", v)
|
||||
}
|
||||
gotPt = append(gotPt, v[0].(string))
|
||||
},
|
||||
buf: new(strings.Builder),
|
||||
}
|
||||
|
||||
var pos int
|
||||
tc.f(func(s string) {
|
||||
_, err := out.Write([]byte(s))
|
||||
if tc.wantErr != nil {
|
||||
if !reflect.DeepEqual(err, tc.wantErr[pos]) {
|
||||
t.Fatalf("Write: error = %v, want %v", err, tc.wantErr[pos])
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatalf("Write: unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
pos++
|
||||
})
|
||||
|
||||
if !slices.Equal(out.msg, tc.want) {
|
||||
t.Errorf("msg: %#v, want %#v", out.msg, tc.want)
|
||||
}
|
||||
|
||||
if out.buf.String() != tc.wantBuf {
|
||||
t.Errorf("buf: %q, want %q", out.buf, tc.wantBuf)
|
||||
}
|
||||
|
||||
wantPt := make([]string, len(tc.wantPt))
|
||||
for i, m := range tc.wantPt {
|
||||
wantPt[i] = tc.prefix + m
|
||||
}
|
||||
if !slices.Equal(gotPt, wantPt) {
|
||||
t.Errorf("passthrough: %#v, want %#v", gotPt, wantPt)
|
||||
}
|
||||
|
||||
wantDump := make([]string, len(tc.want)+len(tc.wantExt))
|
||||
for i, m := range tc.want {
|
||||
wantDump[i] = tc.prefix + m
|
||||
}
|
||||
for i, m := range tc.wantExt {
|
||||
wantDump[len(tc.want)+i] = m
|
||||
}
|
||||
t.Run("dump", func(t *testing.T) {
|
||||
got := make([]string, 0, len(wantDump))
|
||||
out.println = func(v ...any) {
|
||||
if len(v) != 1 {
|
||||
t.Fatalf("Dump: invalid call to println: %#v", v)
|
||||
}
|
||||
got = append(got, v[0].(string))
|
||||
}
|
||||
out.Dump()
|
||||
|
||||
if !slices.Equal(got, wantDump) {
|
||||
t.Errorf("Dump: %#v, want %#v", got, wantDump)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
+73
-1
@@ -1,6 +1,21 @@
|
||||
package system
|
||||
|
||||
import "hakurei.app/system/acl"
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/system/internal/xcb"
|
||||
)
|
||||
|
||||
type osFile interface {
|
||||
Name() string
|
||||
io.Writer
|
||||
fs.File
|
||||
}
|
||||
|
||||
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
|
||||
// syscallDispatcher is embedded in [I], so all methods must be unexported.
|
||||
@@ -10,9 +25,40 @@ type syscallDispatcher interface {
|
||||
// just synchronising access is not enough, as this is for test instrumentation.
|
||||
new(f func(k syscallDispatcher))
|
||||
|
||||
// stat provides os.Stat.
|
||||
stat(name string) (os.FileInfo, error)
|
||||
// open provides [os.Open].
|
||||
open(name string) (osFile, error)
|
||||
// mkdir provides os.Mkdir.
|
||||
mkdir(name string, perm os.FileMode) error
|
||||
// chmod provides os.Chmod.
|
||||
chmod(name string, mode os.FileMode) error
|
||||
// link provides os.Link.
|
||||
link(oldname, newname string) error
|
||||
// remove provides os.Remove.
|
||||
remove(name string) error
|
||||
|
||||
// println provides [log.Println].
|
||||
println(v ...any)
|
||||
|
||||
// aclUpdate provides [acl.Update].
|
||||
aclUpdate(name string, uid int, perms ...acl.Perm) error
|
||||
|
||||
// xcbChangeHosts provides [xcb.ChangeHosts].
|
||||
xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error
|
||||
|
||||
// dbusAddress provides [dbus.Address].
|
||||
dbusAddress() (session, system string)
|
||||
// dbusFinalise provides [dbus.Finalise].
|
||||
dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, system *dbus.Config) (final *dbus.Final, err error)
|
||||
// dbusProxyStart provides the Start method of [dbus.Proxy].
|
||||
dbusProxyStart(proxy *dbus.Proxy) error
|
||||
// dbusProxyClose provides the Close method of [dbus.Proxy].
|
||||
dbusProxyClose(proxy *dbus.Proxy)
|
||||
// dbusProxyWait provides the Wait method of [dbus.Proxy].
|
||||
dbusProxyWait(proxy *dbus.Proxy) error
|
||||
|
||||
isVerbose() bool
|
||||
verbose(v ...any)
|
||||
verbosef(format string, v ...any)
|
||||
}
|
||||
@@ -22,9 +68,35 @@ type direct struct{}
|
||||
|
||||
func (k direct) new(f func(k syscallDispatcher)) { go f(k) }
|
||||
|
||||
func (k direct) stat(name string) (os.FileInfo, error) { return os.Stat(name) }
|
||||
func (k direct) open(name string) (osFile, error) { return os.Open(name) }
|
||||
func (k direct) mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) }
|
||||
func (k direct) chmod(name string, mode os.FileMode) error { return os.Chmod(name, mode) }
|
||||
func (k direct) link(oldname, newname string) error { return os.Link(oldname, newname) }
|
||||
func (k direct) remove(name string) error { return os.Remove(name) }
|
||||
|
||||
func (k direct) println(v ...any) { log.Println(v...) }
|
||||
|
||||
func (k direct) aclUpdate(name string, uid int, perms ...acl.Perm) error {
|
||||
return acl.Update(name, uid, perms...)
|
||||
}
|
||||
|
||||
func (k direct) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error {
|
||||
return xcb.ChangeHosts(mode, family, address)
|
||||
}
|
||||
|
||||
func (k direct) dbusAddress() (session, system string) {
|
||||
return dbus.Address()
|
||||
}
|
||||
|
||||
func (k direct) dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, system *dbus.Config) (final *dbus.Final, err error) {
|
||||
return dbus.Finalise(sessionBus, systemBus, session, system)
|
||||
}
|
||||
|
||||
func (k direct) dbusProxyStart(proxy *dbus.Proxy) error { return proxy.Start() }
|
||||
func (k direct) dbusProxyClose(proxy *dbus.Proxy) { proxy.Close() }
|
||||
func (k direct) dbusProxyWait(proxy *dbus.Proxy) error { return proxy.Wait() }
|
||||
|
||||
func (k direct) isVerbose() bool { return msg.IsVerbose() }
|
||||
func (direct) verbose(v ...any) { msg.Verbose(v...) }
|
||||
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
|
||||
|
||||
+179
-3
@@ -1,12 +1,19 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/system/internal/xcb"
|
||||
)
|
||||
|
||||
// call initialises a [stub.Call].
|
||||
@@ -75,7 +82,7 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
|
||||
type opsBuilderTestCase struct {
|
||||
name string
|
||||
uid int
|
||||
f func(sys *I)
|
||||
f func(t *testing.T, sys *I)
|
||||
want []Op
|
||||
exp stub.Expect
|
||||
}
|
||||
@@ -92,7 +99,7 @@ func checkOpsBuilder(t *testing.T, fname string, testCases []opsBuilderTestCase)
|
||||
|
||||
sys, s := InternalNew(t, tc.exp, tc.uid)
|
||||
defer stub.HandleExit(t)
|
||||
tc.f(sys)
|
||||
tc.f(t, sys)
|
||||
s.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
|
||||
t.Helper()
|
||||
|
||||
@@ -177,6 +184,34 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
|
||||
})
|
||||
}
|
||||
|
||||
type stubFi struct {
|
||||
size int64
|
||||
isDir bool
|
||||
}
|
||||
|
||||
func (stubFi) Name() string { panic("unreachable") }
|
||||
func (fi stubFi) Size() int64 { return fi.size }
|
||||
func (stubFi) Mode() fs.FileMode { panic("unreachable") }
|
||||
func (stubFi) ModTime() time.Time { panic("unreachable") }
|
||||
func (fi stubFi) IsDir() bool { return fi.isDir }
|
||||
func (stubFi) Sys() any { panic("unreachable") }
|
||||
|
||||
type readerOsFile struct {
|
||||
closed bool
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (*readerOsFile) Name() string { panic("unreachable") }
|
||||
func (*readerOsFile) Write([]byte) (int, error) { panic("unreachable") }
|
||||
func (*readerOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
|
||||
func (r *readerOsFile) Close() error {
|
||||
if r.closed {
|
||||
return os.ErrClosed
|
||||
}
|
||||
r.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// InternalNew initialises [I] with a stub syscallDispatcher.
|
||||
func InternalNew(t *testing.T, want stub.Expect, uid int) (*I, *stub.Stub[syscallDispatcher]) {
|
||||
k := stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} }, want)
|
||||
@@ -189,6 +224,63 @@ type kstub struct{ *stub.Stub[syscallDispatcher] }
|
||||
|
||||
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
|
||||
|
||||
func (k *kstub) stat(name string) (fi os.FileInfo, err error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("stat")
|
||||
err = expect.Error(
|
||||
stub.CheckArg(k.Stub, "name", name, 0))
|
||||
if err == nil {
|
||||
fi = expect.Ret.(os.FileInfo)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (k *kstub) open(name string) (f osFile, err error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("open")
|
||||
err = expect.Error(
|
||||
stub.CheckArg(k.Stub, "name", name, 0))
|
||||
if err == nil {
|
||||
f = expect.Ret.(osFile)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (k *kstub) mkdir(name string, perm os.FileMode) error {
|
||||
k.Helper()
|
||||
return k.Expects("mkdir").Error(
|
||||
stub.CheckArg(k.Stub, "name", name, 0),
|
||||
stub.CheckArg(k.Stub, "perm", perm, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) chmod(name string, mode os.FileMode) error {
|
||||
k.Helper()
|
||||
return k.Expects("chmod").Error(
|
||||
stub.CheckArg(k.Stub, "name", name, 0),
|
||||
stub.CheckArg(k.Stub, "mode", mode, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) link(oldname, newname string) error {
|
||||
k.Helper()
|
||||
return k.Expects("link").Error(
|
||||
stub.CheckArg(k.Stub, "oldname", oldname, 0),
|
||||
stub.CheckArg(k.Stub, "newname", newname, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) remove(name string) error {
|
||||
k.Helper()
|
||||
return k.Expects("remove").Error(
|
||||
stub.CheckArg(k.Stub, "name", name, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) println(v ...any) {
|
||||
k.Helper()
|
||||
k.Expects("println")
|
||||
if !stub.CheckArgReflect(k.Stub, "v", v, 0) {
|
||||
k.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kstub) aclUpdate(name string, uid int, perms ...acl.Perm) error {
|
||||
k.Helper()
|
||||
return k.Expects("aclUpdate").Error(
|
||||
@@ -197,9 +289,93 @@ func (k *kstub) aclUpdate(name string, uid int, perms ...acl.Perm) error {
|
||||
stub.CheckArgReflect(k.Stub, "perms", perms, 2))
|
||||
}
|
||||
|
||||
func (k *kstub) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error {
|
||||
k.Helper()
|
||||
return k.Expects("xcbChangeHosts").Error(
|
||||
stub.CheckArg(k.Stub, "mode", mode, 0),
|
||||
stub.CheckArg(k.Stub, "family", family, 1),
|
||||
stub.CheckArg(k.Stub, "address", address, 2))
|
||||
}
|
||||
|
||||
func (k *kstub) dbusAddress() (session, system string) {
|
||||
k.Helper()
|
||||
ret := k.Expects("dbusAddress").Ret.([2]string)
|
||||
return ret[0], ret[1]
|
||||
}
|
||||
|
||||
func (k *kstub) dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, system *dbus.Config) (final *dbus.Final, err error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("dbusFinalise")
|
||||
|
||||
final = expect.Ret.(*dbus.Final)
|
||||
err = expect.Error(
|
||||
stub.CheckArg(k.Stub, "sessionBus", sessionBus, 0),
|
||||
stub.CheckArg(k.Stub, "systemBus", systemBus, 1),
|
||||
stub.CheckArgReflect(k.Stub, "session", session, 2),
|
||||
stub.CheckArgReflect(k.Stub, "system", system, 3))
|
||||
if err != nil {
|
||||
final = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (k *kstub) dbusProxyStart(proxy *dbus.Proxy) error {
|
||||
k.Helper()
|
||||
return k.dbusProxySCW(k.Expects("dbusProxyStart"), proxy)
|
||||
}
|
||||
func (k *kstub) dbusProxyClose(proxy *dbus.Proxy) {
|
||||
k.Helper()
|
||||
if k.dbusProxySCW(k.Expects("dbusProxyClose"), proxy) != nil {
|
||||
k.Fail()
|
||||
}
|
||||
}
|
||||
func (k *kstub) dbusProxyWait(proxy *dbus.Proxy) error {
|
||||
k.Helper()
|
||||
return k.dbusProxySCW(k.Expects("dbusProxyWait"), proxy)
|
||||
}
|
||||
func (k *kstub) dbusProxySCW(expect *stub.Call, proxy *dbus.Proxy) error {
|
||||
k.Helper()
|
||||
v := reflect.ValueOf(proxy).Elem()
|
||||
|
||||
if ctxV := v.FieldByName("ctx"); ctxV.IsNil() {
|
||||
k.Errorf("proxy: ctx = %s", ctxV.String())
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
finalV := v.FieldByName("final")
|
||||
if gotFinal := reflect.NewAt(finalV.Type(), unsafe.Pointer(finalV.UnsafeAddr())).Elem().Interface().(*dbus.Final); !reflect.DeepEqual(gotFinal, expect.Args[0]) {
|
||||
k.Errorf("proxy: final = %#v, want %#v", gotFinal, expect.Args[0])
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
outputV := v.FieldByName("output")
|
||||
if _, ok := reflect.NewAt(outputV.Type(), unsafe.Pointer(outputV.UnsafeAddr())).Elem().Interface().(*linePrefixWriter); !ok {
|
||||
k.Errorf("proxy: output = %s", outputV.String())
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
return expect.Err
|
||||
}
|
||||
|
||||
func (k *kstub) isVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
|
||||
|
||||
// ignoreValue marks a value to be ignored by the test suite.
|
||||
type ignoreValue struct{}
|
||||
|
||||
func (k *kstub) verbose(v ...any) {
|
||||
k.Helper()
|
||||
if k.Expects("verbose").Error(
|
||||
expect := k.Expects("verbose")
|
||||
|
||||
// translate ignores in v
|
||||
if want, ok := expect.Args[0].([]any); ok && len(v) == len(want) {
|
||||
for i, a := range want {
|
||||
if _, ok = a.(ignoreValue); ok {
|
||||
v[i] = ignoreValue{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if expect.Error(
|
||||
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
|
||||
k.FailNow()
|
||||
}
|
||||
|
||||
+17
-18
@@ -2,45 +2,44 @@ package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Link appends [HardlinkOp] to [I] the [Process] criteria.
|
||||
// Link calls LinkFileType with the [Process] criteria.
|
||||
func (sys *I) Link(oldname, newname string) *I { return sys.LinkFileType(Process, oldname, newname) }
|
||||
|
||||
// LinkFileType appends [HardlinkOp] to [I].
|
||||
// LinkFileType maintains a hardlink until its [Enablement] is no longer satisfied.
|
||||
func (sys *I) LinkFileType(et Enablement, oldname, newname string) *I {
|
||||
sys.ops = append(sys.ops, &HardlinkOp{et, newname, oldname})
|
||||
sys.ops = append(sys.ops, &hardlinkOp{et, newname, oldname})
|
||||
return sys
|
||||
}
|
||||
|
||||
// HardlinkOp maintains a hardlink until its [Enablement] is no longer satisfied.
|
||||
type HardlinkOp struct {
|
||||
// hardlinkOp implements [I.LinkFileType].
|
||||
type hardlinkOp struct {
|
||||
et Enablement
|
||||
dst, src string
|
||||
}
|
||||
|
||||
func (l *HardlinkOp) Type() Enablement { return l.et }
|
||||
func (l *hardlinkOp) Type() Enablement { return l.et }
|
||||
|
||||
func (l *HardlinkOp) apply(*I) error {
|
||||
msg.Verbose("linking", l)
|
||||
return newOpError("hardlink", os.Link(l.src, l.dst), false)
|
||||
func (l *hardlinkOp) apply(sys *I) error {
|
||||
sys.verbose("linking", l)
|
||||
return newOpError("hardlink", sys.link(l.src, l.dst), false)
|
||||
}
|
||||
|
||||
func (l *HardlinkOp) revert(_ *I, ec *Criteria) error {
|
||||
func (l *hardlinkOp) revert(sys *I, ec *Criteria) error {
|
||||
if ec.hasType(l.Type()) {
|
||||
msg.Verbosef("removing hard link %q", l.dst)
|
||||
return newOpError("hardlink", os.Remove(l.dst), true)
|
||||
sys.verbosef("removing hard link %q", l.dst)
|
||||
return newOpError("hardlink", sys.remove(l.dst), true)
|
||||
} else {
|
||||
msg.Verbosef("skipping hard link %q", l.dst)
|
||||
sys.verbosef("skipping hard link %q", l.dst)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *HardlinkOp) Is(o Op) bool {
|
||||
target, ok := o.(*HardlinkOp)
|
||||
func (l *hardlinkOp) Is(o Op) bool {
|
||||
target, ok := o.(*hardlinkOp)
|
||||
return ok && l != nil && target != nil && *l == *target
|
||||
}
|
||||
|
||||
func (l *HardlinkOp) Path() string { return l.src }
|
||||
func (l *HardlinkOp) String() string { return fmt.Sprintf("%q from %q", l.dst, l.src) }
|
||||
func (l *hardlinkOp) Path() string { return l.src }
|
||||
func (l *hardlinkOp) String() string { return fmt.Sprintf("%q from %q", l.dst, l.src) }
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
)
|
||||
|
||||
func TestHardlinkOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"link", 0xdeadbeef, 0xff, &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
|
||||
call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, stub.UniqueError(1)),
|
||||
}, &OpError{Op: "hardlink", Err: stub.UniqueError(1)}, nil, nil},
|
||||
|
||||
{"remove", 0xdeadbeef, 0xff, &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
|
||||
call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"removing hard link %q", []any{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}}, nil, nil),
|
||||
call("remove", stub.ExpectArgs{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, stub.UniqueError(0)),
|
||||
}, &OpError{Op: "hardlink", Err: stub.UniqueError(0), Revert: true}},
|
||||
|
||||
{"success skip", 0xdeadbeef, EWayland | EX11, &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
|
||||
call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"skipping hard link %q", []any{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
{"success", 0xdeadbeef, 0xff, &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
|
||||
call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"removing hard link %q", []any{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}}, nil, nil),
|
||||
call("remove", stub.ExpectArgs{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, nil),
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, "LinkFileType", []opsBuilderTestCase{
|
||||
{"type", 0xcafebabe, func(_ *testing.T, sys *I) {
|
||||
sys.LinkFileType(User, "/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse")
|
||||
}, []Op{
|
||||
&hardlinkOp{User, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"},
|
||||
}, stub.Expect{}},
|
||||
|
||||
{"link", 0xcafebabe, func(_ *testing.T, sys *I) {
|
||||
sys.Link("/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse")
|
||||
}, []Op{
|
||||
&hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"},
|
||||
}, stub.Expect{}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"nil", (*hardlinkOp)(nil), (*hardlinkOp)(nil), false},
|
||||
{"zero", new(hardlinkOp), new(hardlinkOp), true},
|
||||
|
||||
{"src differs",
|
||||
&hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse"},
|
||||
&hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"},
|
||||
false},
|
||||
|
||||
{"dst differs",
|
||||
&hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6", "/run/user/1000/pulse/native"},
|
||||
&hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"},
|
||||
false},
|
||||
|
||||
{"et differs",
|
||||
&hardlinkOp{User, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"},
|
||||
&hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"},
|
||||
false},
|
||||
|
||||
{"equals",
|
||||
&hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"},
|
||||
&hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"},
|
||||
true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"link", &hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"},
|
||||
Process, "/run/user/1000/pulse/native",
|
||||
`"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse" from "/run/user/1000/pulse/native"`},
|
||||
})
|
||||
}
|
||||
+19
-21
@@ -6,67 +6,65 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Ensure appends [MkdirOp] to [I] with its [Enablement] ignored.
|
||||
// Ensure ensures the existence of a directory.
|
||||
func (sys *I) Ensure(name string, perm os.FileMode) *I {
|
||||
sys.ops = append(sys.ops, &MkdirOp{User, name, perm, false})
|
||||
sys.ops = append(sys.ops, &mkdirOp{User, name, perm, false})
|
||||
return sys
|
||||
}
|
||||
|
||||
// Ephemeral appends an ephemeral [MkdirOp] to [I].
|
||||
// Ephemeral ensures the existence of a directory until its [Enablement] is no longer satisfied.
|
||||
func (sys *I) Ephemeral(et Enablement, name string, perm os.FileMode) *I {
|
||||
sys.ops = append(sys.ops, &MkdirOp{et, name, perm, true})
|
||||
sys.ops = append(sys.ops, &mkdirOp{et, name, perm, true})
|
||||
return sys
|
||||
}
|
||||
|
||||
// MkdirOp ensures the existence of a directory.
|
||||
// For ephemeral, the directory is destroyed once [Enablement] is no longer satisfied.
|
||||
type MkdirOp struct {
|
||||
// mkdirOp implements [I.Ensure] and [I.Ephemeral].
|
||||
type mkdirOp struct {
|
||||
et Enablement
|
||||
path string
|
||||
perm os.FileMode
|
||||
ephemeral bool
|
||||
}
|
||||
|
||||
func (m *MkdirOp) Type() Enablement { return m.et }
|
||||
func (m *mkdirOp) Type() Enablement { return m.et }
|
||||
|
||||
func (m *MkdirOp) apply(*I) error {
|
||||
msg.Verbose("ensuring directory", m)
|
||||
func (m *mkdirOp) apply(sys *I) error {
|
||||
sys.verbose("ensuring directory", m)
|
||||
|
||||
// create directory
|
||||
if err := os.Mkdir(m.path, m.perm); err != nil {
|
||||
if err := sys.mkdir(m.path, m.perm); err != nil {
|
||||
if !errors.Is(err, os.ErrExist) {
|
||||
return newOpError("mkdir", err, false)
|
||||
}
|
||||
// directory exists, ensure mode
|
||||
return newOpError("mkdir", os.Chmod(m.path, m.perm), false)
|
||||
return newOpError("mkdir", sys.chmod(m.path, m.perm), false)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MkdirOp) revert(_ *I, ec *Criteria) error {
|
||||
func (m *mkdirOp) revert(sys *I, ec *Criteria) error {
|
||||
if !m.ephemeral {
|
||||
// skip non-ephemeral dir and do not log anything
|
||||
return nil
|
||||
}
|
||||
|
||||
if ec.hasType(m.Type()) {
|
||||
msg.Verbose("destroying ephemeral directory", m)
|
||||
return newOpError("mkdir", os.Remove(m.path), true)
|
||||
sys.verbose("destroying ephemeral directory", m)
|
||||
return newOpError("mkdir", sys.remove(m.path), true)
|
||||
} else {
|
||||
msg.Verbose("skipping ephemeral directory", m)
|
||||
sys.verbose("skipping ephemeral directory", m)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MkdirOp) Is(o Op) bool {
|
||||
target, ok := o.(*MkdirOp)
|
||||
func (m *mkdirOp) Is(o Op) bool {
|
||||
target, ok := o.(*mkdirOp)
|
||||
return ok && m != nil && target != nil && *m == *target
|
||||
}
|
||||
|
||||
func (m *MkdirOp) Path() string { return m.path }
|
||||
func (m *mkdirOp) Path() string { return m.path }
|
||||
|
||||
func (m *MkdirOp) String() string {
|
||||
func (m *mkdirOp) String() string {
|
||||
t := "ensure"
|
||||
if m.ephemeral {
|
||||
t = TypeString(m.Type())
|
||||
|
||||
+99
-63
@@ -4,72 +4,108 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
)
|
||||
|
||||
func TestEnsure(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
perm os.FileMode
|
||||
}{
|
||||
{"/tmp/hakurei.1971", 0701},
|
||||
{"/tmp/hakurei.1971/tmpdir", 0700},
|
||||
{"/tmp/hakurei.1971/tmpdir/150", 0700},
|
||||
{"/run/user/1971/hakurei", 0700},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name+"_"+tc.perm.String(), func(t *testing.T) {
|
||||
sys := New(t.Context(), 150)
|
||||
sys.Ensure(tc.name, tc.perm)
|
||||
(&tcOp{User, tc.name}).test(t, sys.ops, []Op{&MkdirOp{User, tc.name, tc.perm, false}}, "Ensure")
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestMkdirOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"mkdir", 0xdeadbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil),
|
||||
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, stub.UniqueError(2)),
|
||||
}, &OpError{Op: "mkdir", Err: stub.UniqueError(2)}, nil, nil},
|
||||
|
||||
func TestEphemeral(t *testing.T) {
|
||||
testCases := []struct {
|
||||
perm os.FileMode
|
||||
tcOp
|
||||
}{
|
||||
{0700, tcOp{Process, "/run/user/1971/hakurei/ec07546a772a07cde87389afc84ffd13"}},
|
||||
{0701, tcOp{Process, "/tmp/hakurei.1971/ec07546a772a07cde87389afc84ffd13"}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.path+"_"+tc.perm.String()+"_"+TypeString(tc.et), func(t *testing.T) {
|
||||
sys := New(t.Context(), 150)
|
||||
sys.Ephemeral(tc.et, tc.path, tc.perm)
|
||||
tc.test(t, sys.ops, []Op{&MkdirOp{tc.et, tc.path, tc.perm, true}}, "Ephemeral")
|
||||
})
|
||||
}
|
||||
}
|
||||
{"chmod", 0xdeadbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil),
|
||||
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, os.ErrExist),
|
||||
call("chmod", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, stub.UniqueError(1)),
|
||||
}, &OpError{Op: "mkdir", Err: stub.UniqueError(1)}, nil, nil},
|
||||
|
||||
func TestMkdirString(t *testing.T) {
|
||||
testCases := []struct {
|
||||
want string
|
||||
ephemeral bool
|
||||
et Enablement
|
||||
}{
|
||||
{"ensure", false, User},
|
||||
{"ensure", false, Process},
|
||||
{"ensure", false, EWayland},
|
||||
{"remove", 0xdeadbeef, 0xff, &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, stub.UniqueError(0)),
|
||||
}, &OpError{Op: "mkdir", Err: stub.UniqueError(0), Revert: true}},
|
||||
|
||||
{"wayland", true, EWayland},
|
||||
{"x11", true, EX11},
|
||||
{"dbus", true, EDBus},
|
||||
{"pulseaudio", true, EPulse},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.want, func(t *testing.T) {
|
||||
m := &MkdirOp{
|
||||
et: tc.et,
|
||||
path: container.Nonexistent,
|
||||
perm: 0701,
|
||||
ephemeral: tc.ephemeral,
|
||||
}
|
||||
want := "mode: " + os.FileMode(0701).String() + " type: " + tc.want + ` path: "/proc/nonexistent"`
|
||||
if got := m.String(); got != want {
|
||||
t.Errorf("String() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
{"success exist chmod", 0xdeadbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil),
|
||||
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, os.ErrExist),
|
||||
call("chmod", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
|
||||
}, nil, nil, nil},
|
||||
|
||||
{"success ensure", 0xdeadbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil),
|
||||
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
|
||||
}, nil, nil, nil},
|
||||
|
||||
{"success skip", 0xdeadbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"skipping ephemeral directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
{"success", 0xdeadbeef, 0xff, &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, nil),
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, "EnsureEphemeral", []opsBuilderTestCase{
|
||||
{"ensure", 0xcafebabe, func(_ *testing.T, sys *I) {
|
||||
sys.Ensure("/tmp/hakurei.0", 0700)
|
||||
}, []Op{
|
||||
&mkdirOp{User, "/tmp/hakurei.0", 0700, false},
|
||||
}, stub.Expect{}},
|
||||
|
||||
{"ephemeral", 0xcafebabe, func(_ *testing.T, sys *I) {
|
||||
sys.Ephemeral(Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711)
|
||||
}, []Op{
|
||||
&mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true},
|
||||
}, stub.Expect{}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"nil", (*mkdirOp)(nil), (*mkdirOp)(nil), false},
|
||||
{"zero", new(mkdirOp), new(mkdirOp), true},
|
||||
|
||||
{"ephemeral differs",
|
||||
&mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, false},
|
||||
&mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
|
||||
false},
|
||||
|
||||
{"perm differs",
|
||||
&mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0701, true},
|
||||
&mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
|
||||
false},
|
||||
|
||||
{"path differs",
|
||||
&mkdirOp{Process, "/tmp/hakurei.1/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
|
||||
&mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
|
||||
false},
|
||||
|
||||
{"et differs",
|
||||
&mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
|
||||
&mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
|
||||
false},
|
||||
|
||||
{"equals",
|
||||
&mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
|
||||
&mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
|
||||
true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"ensure", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, false},
|
||||
User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9",
|
||||
`mode: -rwx------ type: ensure path: "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"`},
|
||||
|
||||
{"ephemeral", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true},
|
||||
User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9",
|
||||
`mode: -rwx------ type: user path: "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"`},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ func (e *OpError) Error() string {
|
||||
|
||||
switch {
|
||||
case errors.As(e.Err, new(*os.PathError)),
|
||||
errors.As(e.Err, new(*os.LinkError)),
|
||||
errors.As(e.Err, new(*net.OpError)),
|
||||
errors.As(e.Err, new(*container.StartError)):
|
||||
return e.Err.Error()
|
||||
|
||||
+6
-81
@@ -137,22 +137,14 @@ func TestPrintJoinedError(t *testing.T) {
|
||||
{"file descriptor in bad state"},
|
||||
}},
|
||||
{"many message", errors.Join(
|
||||
&container.StartError{
|
||||
Step: "meow",
|
||||
Err: syscall.ENOMEM,
|
||||
},
|
||||
&os.PathError{
|
||||
Op: "meow",
|
||||
Path: "/proc/nonexistent",
|
||||
Err: syscall.ENOSYS,
|
||||
},
|
||||
&OpError{
|
||||
Op: "meow",
|
||||
Err: syscall.ENODEV,
|
||||
Revert: true,
|
||||
}), [][]any{
|
||||
&container.StartError{Step: "meow", Err: syscall.ENOMEM},
|
||||
&os.PathError{Op: "meow", Path: "/proc/nonexistent", Err: syscall.ENOSYS},
|
||||
&os.LinkError{Op: "link", Old: "/etc", New: "/proc/nonexistent", Err: syscall.ENOENT},
|
||||
&OpError{Op: "meow", Err: syscall.ENODEV, Revert: true},
|
||||
), [][]any{
|
||||
{"cannot meow: cannot allocate memory"},
|
||||
{"meow /proc/nonexistent: function not implemented"},
|
||||
{"link /etc /proc/nonexistent: no such file or directory"},
|
||||
{"cannot revert meow: no such device"},
|
||||
}},
|
||||
}
|
||||
@@ -166,70 +158,3 @@ func TestPrintJoinedError(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type tcOp struct {
|
||||
et Enablement
|
||||
path string
|
||||
}
|
||||
|
||||
// test an instance of the Op interface
|
||||
func (ptc tcOp) test(t *testing.T, gotOps []Op, wantOps []Op, fn string) {
|
||||
if len(gotOps) != len(wantOps) {
|
||||
t.Errorf("%s: inserted %v Ops, want %v", fn,
|
||||
len(gotOps), len(wantOps))
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("path", func(t *testing.T) {
|
||||
if len(gotOps) > 0 {
|
||||
if got := gotOps[0].Path(); got != ptc.path {
|
||||
t.Errorf("Path() = %q, want %q",
|
||||
got, ptc.path)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for i := range gotOps {
|
||||
o := gotOps[i]
|
||||
|
||||
t.Run("is", func(t *testing.T) {
|
||||
if !o.Is(o) {
|
||||
t.Errorf("Is returned false on self")
|
||||
return
|
||||
}
|
||||
if !o.Is(wantOps[i]) {
|
||||
t.Errorf("%s: inserted %#v, want %#v",
|
||||
fn,
|
||||
o, wantOps[i])
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("criteria", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
ec Enablement
|
||||
want bool
|
||||
}{
|
||||
{"nil", 0xff, ptc.et != User},
|
||||
{"self", ptc.et, true},
|
||||
{"all", EWayland | EX11 | EDBus | EPulse | User | Process, true},
|
||||
{"enablements", EWayland | EX11 | EDBus | EPulse, ptc.et != User && ptc.et != Process},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var criteria *Criteria
|
||||
if tc.ec != 0xff {
|
||||
criteria = (*Criteria)(&tc.ec)
|
||||
}
|
||||
if got := criteria.hasType(o.Type()); got != tc.want {
|
||||
t.Errorf("hasType: got %v, want %v",
|
||||
got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -4,7 +4,6 @@ package system
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -116,14 +115,15 @@ func (sys *I) Commit() error {
|
||||
sys.committed = true
|
||||
|
||||
sp := New(sys.ctx, sys.uid)
|
||||
sp.syscallDispatcher = sys.syscallDispatcher
|
||||
sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits
|
||||
defer func() {
|
||||
// sp is set to nil when all ops are applied
|
||||
if sp != nil {
|
||||
// rollback partial commit
|
||||
msg.Verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops))
|
||||
sys.verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops))
|
||||
if err := sp.Revert(nil); err != nil {
|
||||
printJoinedError(log.Println, "cannot revert partial commit:", err)
|
||||
printJoinedError(sys.println, "cannot revert partial commit:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
+184
-44
@@ -1,35 +1,35 @@
|
||||
package system_test
|
||||
package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
_ "unsafe"
|
||||
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/system/internal/xcb"
|
||||
)
|
||||
|
||||
//go:linkname criteriaHasType hakurei.app/system.(*Criteria).hasType
|
||||
func criteriaHasType(_ *system.Criteria, _ system.Enablement) bool
|
||||
|
||||
func TestCriteria(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
ec, t system.Enablement
|
||||
ec, t Enablement
|
||||
want bool
|
||||
}{
|
||||
{"nil", 0xff, system.EWayland, true},
|
||||
{"nil user", 0xff, system.User, false},
|
||||
{"all", system.EWayland | system.EX11 | system.EDBus | system.EPulse | system.User | system.Process, system.Process, true},
|
||||
{"nil", 0xff, EWayland, true},
|
||||
{"nil user", 0xff, User, false},
|
||||
{"all", EWayland | EX11 | EDBus | EPulse | User | Process, Process, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var criteria *system.Criteria
|
||||
var criteria *Criteria
|
||||
if tc.ec != 0xff {
|
||||
criteria = (*system.Criteria)(&tc.ec)
|
||||
criteria = (*Criteria)(&tc.ec)
|
||||
}
|
||||
if got := criteriaHasType(criteria, tc.t); got != tc.want {
|
||||
if got := criteria.hasType(tc.t); got != tc.want {
|
||||
t.Errorf("hasType: got %v, want %v",
|
||||
got, tc.want)
|
||||
}
|
||||
@@ -39,23 +39,23 @@ func TestCriteria(t *testing.T) {
|
||||
|
||||
func TestTypeString(t *testing.T) {
|
||||
testCases := []struct {
|
||||
e system.Enablement
|
||||
e Enablement
|
||||
want string
|
||||
}{
|
||||
{system.EWayland, system.EWayland.String()},
|
||||
{system.EX11, system.EX11.String()},
|
||||
{system.EDBus, system.EDBus.String()},
|
||||
{system.EPulse, system.EPulse.String()},
|
||||
{system.User, "user"},
|
||||
{system.Process, "process"},
|
||||
{system.User | system.Process, "user, process"},
|
||||
{system.EWayland | system.User | system.Process, "wayland, user, process"},
|
||||
{system.EX11 | system.Process, "x11, process"},
|
||||
{EWayland, EWayland.String()},
|
||||
{EX11, EX11.String()},
|
||||
{EDBus, EDBus.String()},
|
||||
{EPulse, EPulse.String()},
|
||||
{User, "user"},
|
||||
{Process, "process"},
|
||||
{User | Process, "user, process"},
|
||||
{EWayland | User | Process, "wayland, user, process"},
|
||||
{EX11 | Process, "x11, process"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run("label type string "+strconv.Itoa(int(tc.e)), func(t *testing.T) {
|
||||
if got := system.TypeString(tc.e); got != tc.want {
|
||||
if got := TypeString(tc.e); got != tc.want {
|
||||
t.Errorf("TypeString: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
@@ -71,7 +71,7 @@ func TestNew(t *testing.T) {
|
||||
t.Errorf("recover: %v, want %v", r, want)
|
||||
}
|
||||
}()
|
||||
system.New(nil, 0)
|
||||
New(nil, 0)
|
||||
})
|
||||
|
||||
t.Run("uid", func(t *testing.T) {
|
||||
@@ -81,13 +81,13 @@ func TestNew(t *testing.T) {
|
||||
t.Errorf("recover: %v, want %v", r, want)
|
||||
}
|
||||
}()
|
||||
system.New(t.Context(), -1)
|
||||
New(t.Context(), -1)
|
||||
})
|
||||
})
|
||||
|
||||
sys := system.New(t.Context(), 0xdeadbeef)
|
||||
if got := reflect.ValueOf(sys).Elem().FieldByName("ctx"); got.IsNil() {
|
||||
t.Errorf("New: ctx = %#v", got)
|
||||
sys := New(t.Context(), 0xdeadbeef)
|
||||
if sys.ctx == nil {
|
||||
t.Error("New: ctx = nil")
|
||||
}
|
||||
if got := sys.UID(); got != 0xdeadbeef {
|
||||
t.Errorf("UID: %d", got)
|
||||
@@ -97,56 +97,56 @@ func TestNew(t *testing.T) {
|
||||
func TestEqual(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
sys *system.I
|
||||
v *system.I
|
||||
sys *I
|
||||
v *I
|
||||
want bool
|
||||
}{
|
||||
{"simple UID",
|
||||
system.New(t.Context(), 150),
|
||||
system.New(t.Context(), 150),
|
||||
New(t.Context(), 150),
|
||||
New(t.Context(), 150),
|
||||
true},
|
||||
|
||||
{"simple UID differ",
|
||||
system.New(t.Context(), 150),
|
||||
system.New(t.Context(), 151),
|
||||
New(t.Context(), 150),
|
||||
New(t.Context(), 151),
|
||||
false},
|
||||
|
||||
{"simple UID nil",
|
||||
system.New(t.Context(), 150),
|
||||
New(t.Context(), 150),
|
||||
nil,
|
||||
false},
|
||||
|
||||
{"op length mismatch",
|
||||
system.New(t.Context(), 150).
|
||||
New(t.Context(), 150).
|
||||
ChangeHosts("chronos"),
|
||||
system.New(t.Context(), 150).
|
||||
New(t.Context(), 150).
|
||||
ChangeHosts("chronos").
|
||||
Ensure("/run", 0755),
|
||||
false},
|
||||
|
||||
{"op value mismatch",
|
||||
system.New(t.Context(), 150).
|
||||
New(t.Context(), 150).
|
||||
ChangeHosts("chronos").
|
||||
Ensure("/run", 0644),
|
||||
system.New(t.Context(), 150).
|
||||
New(t.Context(), 150).
|
||||
ChangeHosts("chronos").
|
||||
Ensure("/run", 0755),
|
||||
false},
|
||||
|
||||
{"op type mismatch",
|
||||
system.New(t.Context(), 150).
|
||||
New(t.Context(), 150).
|
||||
ChangeHosts("chronos").
|
||||
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 0, 256),
|
||||
system.New(t.Context(), 150).
|
||||
New(t.Context(), 150).
|
||||
ChangeHosts("chronos").
|
||||
Ensure("/run", 0755),
|
||||
false},
|
||||
|
||||
{"op equals",
|
||||
system.New(t.Context(), 150).
|
||||
New(t.Context(), 150).
|
||||
ChangeHosts("chronos").
|
||||
Ensure("/run", 0755),
|
||||
system.New(t.Context(), 150).
|
||||
New(t.Context(), 150).
|
||||
ChangeHosts("chronos").
|
||||
Ensure("/run", 0755),
|
||||
true},
|
||||
@@ -160,3 +160,143 @@ func TestEqual(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitRevert(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
f func(sys *I)
|
||||
ec Enablement
|
||||
|
||||
commit []stub.Call
|
||||
wantErrCommit error
|
||||
|
||||
revert []stub.Call
|
||||
wantErrRevert error
|
||||
}{
|
||||
{"apply xhost partial mkdir", func(sys *I) {
|
||||
sys.
|
||||
Ephemeral(Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711).
|
||||
ChangeHosts("chronos")
|
||||
}, 0xff, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(2)),
|
||||
call("verbosef", stub.ExpectArgs{"commit faulted after %d ops, rolling back partial commit", []any{1}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, stub.UniqueError(3)),
|
||||
call("println", stub.ExpectArgs{[]any{"cannot revert mkdir: unique error 3 injected by the test suite"}}, nil, nil),
|
||||
}, &OpError{Op: "xhost", Err: stub.UniqueError(2)}, nil, nil},
|
||||
|
||||
{"apply xhost", func(sys *I) {
|
||||
sys.
|
||||
Ephemeral(Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711).
|
||||
ChangeHosts("chronos")
|
||||
}, 0xff, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(2)),
|
||||
call("verbosef", stub.ExpectArgs{"commit faulted after %d ops, rolling back partial commit", []any{1}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, nil),
|
||||
}, &OpError{Op: "xhost", Err: stub.UniqueError(2)}, nil, nil},
|
||||
|
||||
{"revert multi", func(sys *I) {
|
||||
sys.
|
||||
Ephemeral(Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711).
|
||||
ChangeHosts("chronos")
|
||||
}, 0xff, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"deleting entry %s from X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeDelete), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(1)),
|
||||
call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, stub.UniqueError(0)),
|
||||
}, errors.Join(
|
||||
&OpError{Op: "xhost", Err: stub.UniqueError(1), Revert: true},
|
||||
&OpError{Op: "mkdir", Err: stub.UniqueError(0), Revert: true})},
|
||||
|
||||
{"success", func(sys *I) {
|
||||
sys.
|
||||
Ephemeral(Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711).
|
||||
ChangeHosts("chronos")
|
||||
}, 0xff, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"deleting entry %s from X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeDelete), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil),
|
||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, nil),
|
||||
}, nil},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var ec *Criteria
|
||||
if tc.ec != 0xff {
|
||||
ec = (*Criteria)(&tc.ec)
|
||||
}
|
||||
|
||||
sys, s := InternalNew(t, stub.Expect{Calls: slices.Concat(tc.commit, []stub.Call{{Name: stub.CallSeparator}}, tc.revert)}, 0xbad)
|
||||
defer stub.HandleExit(t)
|
||||
tc.f(sys)
|
||||
errCommit := sys.Commit()
|
||||
s.Expects(stub.CallSeparator)
|
||||
if !reflect.DeepEqual(errCommit, tc.wantErrCommit) {
|
||||
t.Errorf("Commit: error = %v, want %v", errCommit, tc.wantErrCommit)
|
||||
}
|
||||
if errCommit != nil {
|
||||
goto out
|
||||
}
|
||||
|
||||
if err := sys.Revert(ec); !reflect.DeepEqual(err, tc.wantErrRevert) {
|
||||
t.Errorf("Revert: error = %v, want %v", err, tc.wantErrRevert)
|
||||
}
|
||||
|
||||
out:
|
||||
s.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
|
||||
count := s.Pos() - 1 // separator
|
||||
if count < len(tc.commit) {
|
||||
t.Errorf("Commit: %d calls, want %d", count, len(tc.commit))
|
||||
} else {
|
||||
t.Errorf("Revert: %d calls, want %d", count-len(tc.commit), len(tc.revert))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("panic", func(t *testing.T) {
|
||||
t.Run("committed", func(t *testing.T) {
|
||||
defer func() {
|
||||
want := "attempting to commit twice"
|
||||
if r := recover(); r != want {
|
||||
t.Errorf("Commit: panic = %v, want %v", r, want)
|
||||
}
|
||||
}()
|
||||
_ = (&I{committed: true}).Commit()
|
||||
})
|
||||
|
||||
t.Run("reverted", func(t *testing.T) {
|
||||
defer func() {
|
||||
want := "attempting to revert twice"
|
||||
if r := recover(); r != want {
|
||||
t.Errorf("Revert: panic = %v, want %v", r, want)
|
||||
}
|
||||
}()
|
||||
_ = (&I{reverted: true}).Revert(nil)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestNop(t *testing.T) {
|
||||
// these do nothing
|
||||
new(noCopy).Unlock()
|
||||
new(noCopy).Lock()
|
||||
}
|
||||
|
||||
+29
-17
@@ -9,16 +9,16 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// CopyFile appends [TmpfileOp] to [I].
|
||||
func (sys *I) CopyFile(payload *[]byte, src string, cap int, n int64) *I {
|
||||
// CopyFile reads up to n bytes from src and writes the resulting byte slice to payloadP.
|
||||
func (sys *I) CopyFile(payloadP *[]byte, src string, cap int, n int64) *I {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.Grow(cap)
|
||||
sys.ops = append(sys.ops, &TmpfileOp{payload, src, n, buf})
|
||||
sys.ops = append(sys.ops, &tmpfileOp{payloadP, src, n, buf})
|
||||
return sys
|
||||
}
|
||||
|
||||
// TmpfileOp reads up to n bytes from src and writes the resulting byte slice to payload.
|
||||
type TmpfileOp struct {
|
||||
// tmpfileOp implements [I.CopyFile].
|
||||
type tmpfileOp struct {
|
||||
payload *[]byte
|
||||
src string
|
||||
|
||||
@@ -26,16 +26,17 @@ type TmpfileOp struct {
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func (t *TmpfileOp) Type() Enablement { return Process }
|
||||
func (t *TmpfileOp) apply(*I) error {
|
||||
msg.Verbose("copying", t)
|
||||
func (t *tmpfileOp) Type() Enablement { return Process }
|
||||
|
||||
func (t *tmpfileOp) apply(sys *I) error {
|
||||
if t.payload == nil {
|
||||
// this is a misuse of the API; do not return an error message
|
||||
// this is a misuse of the API; do not return a wrapped error
|
||||
return errors.New("invalid payload")
|
||||
}
|
||||
|
||||
if b, err := os.Stat(t.src); err != nil {
|
||||
sys.verbose("copying", t)
|
||||
|
||||
if b, err := sys.stat(t.src); err != nil {
|
||||
return newOpError("tmpfile", err, false)
|
||||
} else {
|
||||
if b.IsDir() {
|
||||
@@ -46,21 +47,32 @@ func (t *TmpfileOp) apply(*I) error {
|
||||
}
|
||||
}
|
||||
|
||||
if f, err := os.Open(t.src); err != nil {
|
||||
var r io.ReadCloser
|
||||
if f, err := sys.open(t.src); err != nil {
|
||||
return newOpError("tmpfile", err, false)
|
||||
} else if _, err = io.CopyN(t.buf, f, t.n); err != nil {
|
||||
} else {
|
||||
r = f
|
||||
}
|
||||
if n, err := io.CopyN(t.buf, r, t.n); err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
_ = r.Close()
|
||||
return newOpError("tmpfile", err, false)
|
||||
}
|
||||
sys.verbosef("copied %d bytes from %q", n, t.src)
|
||||
}
|
||||
if err := r.Close(); err != nil {
|
||||
return newOpError("tmpfile", err, false)
|
||||
}
|
||||
|
||||
*t.payload = t.buf.Bytes()
|
||||
return nil
|
||||
}
|
||||
func (t *TmpfileOp) revert(*I, *Criteria) error { t.buf.Reset(); return nil }
|
||||
func (t *tmpfileOp) revert(*I, *Criteria) error { t.buf.Reset(); return nil }
|
||||
|
||||
func (t *TmpfileOp) Is(o Op) bool {
|
||||
target, ok := o.(*TmpfileOp)
|
||||
func (t *tmpfileOp) Is(o Op) bool {
|
||||
target, ok := o.(*tmpfileOp)
|
||||
return ok && t != nil && target != nil &&
|
||||
t.src == target.src && t.n == target.n
|
||||
}
|
||||
func (t *TmpfileOp) Path() string { return t.src }
|
||||
func (t *TmpfileOp) String() string { return fmt.Sprintf("up to %d bytes from %q", t.n, t.src) }
|
||||
func (t *tmpfileOp) Path() string { return t.src }
|
||||
func (t *tmpfileOp) String() string { return fmt.Sprintf("up to %d bytes from %q", t.n, t.src) }
|
||||
|
||||
+131
-70
@@ -1,81 +1,142 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
)
|
||||
|
||||
func TestCopyFile(t *testing.T) {
|
||||
testCases := []struct {
|
||||
tcOp
|
||||
cap int
|
||||
n int64
|
||||
}{
|
||||
{tcOp{Process, "/home/ophestra/xdg/config/pulse/cookie"}, 256, 256},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run("copy file "+tc.path+" with cap = "+strconv.Itoa(tc.cap)+" n = "+strconv.Itoa(int(tc.n)), func(t *testing.T) {
|
||||
sys := New(t.Context(), 150)
|
||||
sys.CopyFile(new([]byte), tc.path, tc.cap, tc.n)
|
||||
tc.test(t, sys.ops, []Op{
|
||||
&TmpfileOp{nil, tc.path, tc.n, nil},
|
||||
}, "CopyFile")
|
||||
})
|
||||
}
|
||||
}
|
||||
type errorReader struct{}
|
||||
|
||||
func TestLink(t *testing.T) {
|
||||
testCases := []struct {
|
||||
dst, src string
|
||||
}{
|
||||
{"/tmp/hakurei.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"},
|
||||
{"/tmp/hakurei.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run("link file "+tc.dst+" from "+tc.src, func(t *testing.T) {
|
||||
sys := New(t.Context(), 150)
|
||||
sys.Link(tc.src, tc.dst)
|
||||
(&tcOp{Process, tc.src}).test(t, sys.ops, []Op{
|
||||
&HardlinkOp{Process, tc.dst, tc.src},
|
||||
}, "Link")
|
||||
})
|
||||
}
|
||||
}
|
||||
func (errorReader) Read([]byte) (int, error) { return 0, stub.UniqueError(0xdeadbeef) }
|
||||
|
||||
func TestLinkFileType(t *testing.T) {
|
||||
testCases := []struct {
|
||||
tcOp
|
||||
dst string
|
||||
}{
|
||||
{tcOp{User, "/tmp/hakurei.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"},
|
||||
{tcOp{Process, "/tmp/hakurei.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run("link file "+tc.dst+" from "+tc.path+" with type "+TypeString(tc.et), func(t *testing.T) {
|
||||
sys := New(t.Context(), 150)
|
||||
sys.LinkFileType(tc.et, tc.path, tc.dst)
|
||||
tc.test(t, sys.ops, []Op{
|
||||
&HardlinkOp{tc.et, tc.dst, tc.path},
|
||||
}, "LinkFileType")
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestTmpfileOp(t *testing.T) {
|
||||
// 255 bytes
|
||||
const paSample = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
|
||||
func TestTmpfile_String(t *testing.T) {
|
||||
testCases := []struct {
|
||||
src string
|
||||
n int64
|
||||
want string
|
||||
}{
|
||||
{"/home/ophestra/xdg/config/pulse/cookie", 256,
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"payload", 0xdead, 0xff, &tmpfileOp{
|
||||
nil, "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
|
||||
func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
|
||||
}, nil, errors.New("invalid payload"), nil, nil},
|
||||
|
||||
{"stat", 0xdead, 0xff, &tmpfileOp{
|
||||
new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
|
||||
func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
|
||||
}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, nil, stub.UniqueError(1)),
|
||||
}, &OpError{Op: "tmpfile", Err: stub.UniqueError(1)}, nil, nil},
|
||||
|
||||
{"stat EISDIR", 0xdead, 0xff, &tmpfileOp{
|
||||
new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
|
||||
func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
|
||||
}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, true}, nil),
|
||||
}, &OpError{Op: "tmpfile", Err: &os.PathError{Op: "stat", Path: "/home/ophestra/xdg/config/pulse/cookie", Err: syscall.EISDIR}}, nil, nil},
|
||||
|
||||
{"stat ENOMEM", 0xdead, 0xff, &tmpfileOp{
|
||||
new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
|
||||
func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
|
||||
}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1<<8 + 1, false}, nil),
|
||||
}, &OpError{Op: "tmpfile", Err: &os.PathError{Op: "stat", Path: "/home/ophestra/xdg/config/pulse/cookie", Err: syscall.ENOMEM}}, nil, nil},
|
||||
|
||||
{"open", 0xdead, 0xff, &tmpfileOp{
|
||||
new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
|
||||
func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
|
||||
}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil),
|
||||
call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, nil, stub.UniqueError(0)),
|
||||
}, &OpError{Op: "tmpfile", Err: stub.UniqueError(0)}, nil, nil},
|
||||
|
||||
{"reader", 0xdead, 0xff, &tmpfileOp{
|
||||
new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
|
||||
func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
|
||||
}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil),
|
||||
call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &readerOsFile{true, errorReader{}}, nil),
|
||||
}, &OpError{Op: "tmpfile", Err: stub.UniqueError(0xdeadbeef)}, nil, nil},
|
||||
|
||||
{"closed", 0xdead, 0xff, &tmpfileOp{
|
||||
new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
|
||||
func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
|
||||
}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil),
|
||||
call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &readerOsFile{true, strings.NewReader(paSample + "=")}, nil),
|
||||
}, &OpError{Op: "tmpfile", Err: os.ErrClosed}, nil, nil},
|
||||
|
||||
{"success full", 0xdead, 0xff, &tmpfileOp{
|
||||
new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
|
||||
func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
|
||||
}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil),
|
||||
call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &readerOsFile{false, strings.NewReader(paSample + "=")}, nil),
|
||||
}, nil, nil, nil},
|
||||
|
||||
{"success", 0xdead, 0xff, &tmpfileOp{
|
||||
new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
|
||||
func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
|
||||
}, []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil),
|
||||
call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &readerOsFile{false, strings.NewReader(paSample)}, nil),
|
||||
call("verbosef", stub.ExpectArgs{"copied %d bytes from %q", []any{int64(1<<8 - 1), "/home/ophestra/xdg/config/pulse/cookie"}}, nil, nil),
|
||||
}, nil, nil, nil},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, "CopyFile", []opsBuilderTestCase{
|
||||
{"pulse", 0xcafebabe, func(_ *testing.T, sys *I) {
|
||||
sys.CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1<<8, 1<<8)
|
||||
}, []Op{&tmpfileOp{
|
||||
new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
|
||||
func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
|
||||
}}, stub.Expect{}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"nil", (*tmpfileOp)(nil), (*tmpfileOp)(nil), false},
|
||||
{"zero", new(tmpfileOp), new(tmpfileOp), true},
|
||||
|
||||
{"n differs", &tmpfileOp{
|
||||
src: "/home/ophestra/xdg/config/pulse/cookie",
|
||||
n: 1 << 7,
|
||||
}, &tmpfileOp{
|
||||
src: "/home/ophestra/xdg/config/pulse/cookie",
|
||||
n: 1 << 8,
|
||||
}, false},
|
||||
|
||||
{"src differs", &tmpfileOp{
|
||||
src: "/home/ophestra/xdg/config/pulse",
|
||||
n: 1 << 8,
|
||||
}, &tmpfileOp{
|
||||
src: "/home/ophestra/xdg/config/pulse/cookie",
|
||||
n: 1 << 8,
|
||||
}, false},
|
||||
|
||||
{"equals", &tmpfileOp{
|
||||
src: "/home/ophestra/xdg/config/pulse/cookie",
|
||||
n: 1 << 8,
|
||||
}, &tmpfileOp{
|
||||
src: "/home/ophestra/xdg/config/pulse/cookie",
|
||||
n: 1 << 8,
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"pulse", &tmpfileOp{nil, "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, nil},
|
||||
Process, "/home/ophestra/xdg/config/pulse/cookie",
|
||||
`up to 256 bytes from "/home/ophestra/xdg/config/pulse/cookie"`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.want, func(t *testing.T) {
|
||||
if got := (&TmpfileOp{src: tc.src, n: tc.n}).String(); got != tc.want {
|
||||
t.Errorf("String() = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+30
-35
@@ -9,76 +9,71 @@ import (
|
||||
"hakurei.app/system/wayland"
|
||||
)
|
||||
|
||||
// Wayland appends [WaylandOp] to [I].
|
||||
type waylandConn interface {
|
||||
Attach(p string) (err error)
|
||||
Bind(pathname, appID, instanceID string) (*os.File, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Wayland maintains a wayland socket with security-context-v1 attached via [wayland].
|
||||
// The socket stops accepting connections once the pipe referred to by sync is closed.
|
||||
// The socket is pathname only and is destroyed on revert.
|
||||
func (sys *I) Wayland(syncFd **os.File, dst, src, appID, instanceID string) *I {
|
||||
sys.ops = append(sys.ops, &WaylandOp{syncFd, dst, src, appID, instanceID, wayland.Conn{}})
|
||||
sys.ops = append(sys.ops, &waylandOp{syncFd, dst, src, appID, instanceID, new(wayland.Conn)})
|
||||
return sys
|
||||
}
|
||||
|
||||
// WaylandOp maintains a wayland socket with security-context-v1 attached via [wayland].
|
||||
// The socket stops accepting connections once the pipe referred to by sync is closed.
|
||||
// The socket is pathname only and is destroyed on revert.
|
||||
type WaylandOp struct {
|
||||
// waylandOp implements [I.Wayland].
|
||||
type waylandOp struct {
|
||||
sync **os.File
|
||||
dst, src string
|
||||
appID, instanceID string
|
||||
|
||||
conn wayland.Conn
|
||||
conn waylandConn
|
||||
}
|
||||
|
||||
func (w *WaylandOp) Type() Enablement { return Process }
|
||||
func (w *waylandOp) Type() Enablement { return Process }
|
||||
|
||||
func (w *WaylandOp) apply(sys *I) error {
|
||||
func (w *waylandOp) apply(sys *I) error {
|
||||
if w.sync == nil {
|
||||
// this is a misuse of the API; do not return a wrapped error
|
||||
return errors.New("invalid sync")
|
||||
}
|
||||
|
||||
// the Wayland op is not repeatable
|
||||
if *w.sync != nil {
|
||||
// this is a misuse of the API; do not return a wrapped error
|
||||
return errors.New("attempted to attach multiple wayland sockets")
|
||||
}
|
||||
|
||||
if err := w.conn.Attach(w.src); err != nil {
|
||||
return newOpError("wayland", err, false)
|
||||
} else {
|
||||
msg.Verbosef("wayland attached on %q", w.src)
|
||||
sys.verbosef("wayland attached on %q", w.src)
|
||||
}
|
||||
|
||||
if sp, err := w.conn.Bind(w.dst, w.appID, w.instanceID); err != nil {
|
||||
return newOpError("wayland", err, false)
|
||||
} else {
|
||||
*w.sync = sp
|
||||
msg.Verbosef("wayland listening on %q", w.dst)
|
||||
if err = os.Chmod(w.dst, 0); err != nil {
|
||||
sys.verbosef("wayland listening on %q", w.dst)
|
||||
if err = sys.chmod(w.dst, 0); err != nil {
|
||||
return newOpError("wayland", err, false)
|
||||
}
|
||||
return newOpError("wayland", acl.Update(w.dst, sys.uid, acl.Read, acl.Write, acl.Execute), false)
|
||||
return newOpError("wayland", sys.aclUpdate(w.dst, sys.uid, acl.Read, acl.Write, acl.Execute), false)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WaylandOp) revert(_ *I, ec *Criteria) error {
|
||||
if ec.hasType(w.Type()) {
|
||||
msg.Verbosef("removing wayland socket on %q", w.dst)
|
||||
if err := os.Remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return newOpError("wayland", err, true)
|
||||
}
|
||||
|
||||
msg.Verbosef("detaching from wayland on %q", w.src)
|
||||
return newOpError("wayland", w.conn.Close(), true)
|
||||
} else {
|
||||
msg.Verbosef("skipping wayland cleanup on %q", w.dst)
|
||||
return nil
|
||||
func (w *waylandOp) revert(sys *I, _ *Criteria) error {
|
||||
sys.verbosef("removing wayland socket on %q", w.dst)
|
||||
if err := sys.remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return newOpError("wayland", err, true)
|
||||
}
|
||||
|
||||
sys.verbosef("detaching from wayland on %q", w.src)
|
||||
return newOpError("wayland", w.conn.Close(), true)
|
||||
}
|
||||
|
||||
func (w *WaylandOp) Is(o Op) bool {
|
||||
target, ok := o.(*WaylandOp)
|
||||
func (w *waylandOp) Is(o Op) bool {
|
||||
target, ok := o.(*waylandOp)
|
||||
return ok && w != nil && target != nil &&
|
||||
w.dst == target.dst && w.src == target.src &&
|
||||
w.appID == target.appID && w.instanceID == target.instanceID
|
||||
}
|
||||
|
||||
func (w *WaylandOp) Path() string { return w.dst }
|
||||
func (w *WaylandOp) String() string { return fmt.Sprintf("wayland socket at %q", w.dst) }
|
||||
func (w *waylandOp) Path() string { return w.dst }
|
||||
func (w *waylandOp) String() string { return fmt.Sprintf("wayland socket at %q", w.dst) }
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/wayland"
|
||||
)
|
||||
|
||||
type stubWaylandConn struct {
|
||||
t *testing.T
|
||||
|
||||
wantAttach string
|
||||
attachErr error
|
||||
attached bool
|
||||
|
||||
wantBind [3]string
|
||||
bindErr error
|
||||
bound bool
|
||||
|
||||
closeErr error
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (conn *stubWaylandConn) Attach(p string) (err error) {
|
||||
conn.t.Helper()
|
||||
|
||||
if conn.attached {
|
||||
conn.t.Fatal("Attach called twice")
|
||||
}
|
||||
conn.attached = true
|
||||
|
||||
err = conn.attachErr
|
||||
if p != conn.wantAttach {
|
||||
conn.t.Errorf("Attach: p = %q, want %q", p, conn.wantAttach)
|
||||
err = stub.ErrCheck
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (conn *stubWaylandConn) Bind(pathname, appID, instanceID string) (*os.File, error) {
|
||||
conn.t.Helper()
|
||||
|
||||
if !conn.attached {
|
||||
conn.t.Fatal("Bind called before Attach")
|
||||
}
|
||||
|
||||
if conn.bound {
|
||||
conn.t.Fatal("Bind called twice")
|
||||
}
|
||||
conn.bound = true
|
||||
|
||||
if pathname != conn.wantBind[0] {
|
||||
conn.t.Errorf("Attach: pathname = %q, want %q", pathname, conn.wantBind[0])
|
||||
return nil, stub.ErrCheck
|
||||
}
|
||||
if appID != conn.wantBind[1] {
|
||||
conn.t.Errorf("Attach: appID = %q, want %q", appID, conn.wantBind[1])
|
||||
return nil, stub.ErrCheck
|
||||
}
|
||||
if instanceID != conn.wantBind[2] {
|
||||
conn.t.Errorf("Attach: instanceID = %q, want %q", instanceID, conn.wantBind[2])
|
||||
return nil, stub.ErrCheck
|
||||
}
|
||||
return nil, conn.bindErr
|
||||
}
|
||||
|
||||
func (conn *stubWaylandConn) Close() error {
|
||||
conn.t.Helper()
|
||||
|
||||
if !conn.attached {
|
||||
conn.t.Fatal("Close called before Attach")
|
||||
}
|
||||
if !conn.bound {
|
||||
conn.t.Fatal("Close called before Bind")
|
||||
}
|
||||
|
||||
if conn.closed {
|
||||
conn.t.Fatal("Close called twice")
|
||||
}
|
||||
conn.closed = true
|
||||
return conn.closeErr
|
||||
}
|
||||
|
||||
func TestWaylandOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"invalid sync", 0xdeadbeef, 0xff, &waylandOp{
|
||||
nil,
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
nil,
|
||||
}, nil, errors.New("invalid sync"), nil, nil},
|
||||
|
||||
{"attach", 0xdeadbeef, 0xff, &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
&stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"},
|
||||
attachErr: stub.UniqueError(5)},
|
||||
}, nil, &OpError{Op: "wayland", Err: stub.UniqueError(5)}, nil, nil},
|
||||
|
||||
{"bind", 0xdeadbeef, 0xff, &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
&stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"},
|
||||
bindErr: stub.UniqueError(4)},
|
||||
}, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"wayland attached on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil),
|
||||
}, &OpError{Op: "wayland", Err: stub.UniqueError(4)}, nil, nil},
|
||||
|
||||
{"chmod", 0xdeadbeef, 0xff, &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
&stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}},
|
||||
}, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"wayland attached on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"wayland listening on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil),
|
||||
call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, stub.UniqueError(3)),
|
||||
}, &OpError{Op: "wayland", Err: stub.UniqueError(3)}, nil, nil},
|
||||
|
||||
{"aclUpdate", 0xdeadbeef, 0xff, &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
&stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}},
|
||||
}, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"wayland attached on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"wayland listening on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil),
|
||||
call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil),
|
||||
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(2)),
|
||||
}, &OpError{Op: "wayland", Err: stub.UniqueError(2)}, nil, nil},
|
||||
|
||||
{"remove", 0xdeadbeef, 0xff, &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
&stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}},
|
||||
}, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"wayland attached on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"wayland listening on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil),
|
||||
call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil),
|
||||
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"removing wayland socket on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil),
|
||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}, nil, stub.UniqueError(1)),
|
||||
}, &OpError{Op: "wayland", Err: stub.UniqueError(1), Revert: true}},
|
||||
|
||||
{"close", 0xdeadbeef, 0xff, &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
&stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"},
|
||||
closeErr: stub.UniqueError(0)},
|
||||
}, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"wayland attached on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"wayland listening on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil),
|
||||
call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil),
|
||||
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"removing wayland socket on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil),
|
||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"detaching from wayland on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil),
|
||||
}, &OpError{Op: "wayland", Err: stub.UniqueError(0), Revert: true}},
|
||||
|
||||
{"success", 0xdeadbeef, 0xff, &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
&stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}},
|
||||
}, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"wayland attached on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"wayland listening on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil),
|
||||
call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil),
|
||||
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"removing wayland socket on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil),
|
||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"detaching from wayland on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil),
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, "Wayland", []opsBuilderTestCase{
|
||||
{"chromium", 0xcafe, func(_ *testing.T, sys *I) {
|
||||
sys.Wayland(
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
)
|
||||
}, []Op{&waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
new(wayland.Conn),
|
||||
}}, stub.Expect{}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"dst differs", &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7d/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
new(wayland.Conn),
|
||||
}, &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
new(wayland.Conn),
|
||||
}, false},
|
||||
|
||||
{"src differs", &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-1",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
new(wayland.Conn),
|
||||
}, &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
new(wayland.Conn),
|
||||
}, false},
|
||||
|
||||
{"appID differs", &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
new(wayland.Conn),
|
||||
}, &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
new(wayland.Conn),
|
||||
}, false},
|
||||
|
||||
{"instanceID differs", &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7d",
|
||||
new(wayland.Conn),
|
||||
}, &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
new(wayland.Conn),
|
||||
}, false},
|
||||
|
||||
{"equals", &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
new(wayland.Conn),
|
||||
}, &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
new(wayland.Conn),
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"chromium", &waylandOp{
|
||||
new(*os.File),
|
||||
"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
"/run/user/1971/wayland-0",
|
||||
"org.chromium.Chromium",
|
||||
"ebf083d1b175911782d413369b64ce7c",
|
||||
new(wayland.Conn),
|
||||
}, Process, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland",
|
||||
`wayland socket at "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"`},
|
||||
})
|
||||
}
|
||||
+15
-15
@@ -4,34 +4,34 @@ import (
|
||||
"hakurei.app/system/internal/xcb"
|
||||
)
|
||||
|
||||
// ChangeHosts appends [XHostOp] to [I].
|
||||
// ChangeHosts inserts the target user into X11 hosts and deletes it once its [Enablement] is no longer satisfied.
|
||||
func (sys *I) ChangeHosts(username string) *I {
|
||||
sys.ops = append(sys.ops, XHostOp(username))
|
||||
sys.ops = append(sys.ops, xhostOp(username))
|
||||
return sys
|
||||
}
|
||||
|
||||
// XHostOp inserts the target user into X11 hosts and deletes it once its [Enablement] is no longer satisfied.
|
||||
type XHostOp string
|
||||
// xhostOp implements [I.ChangeHosts].
|
||||
type xhostOp string
|
||||
|
||||
func (x XHostOp) Type() Enablement { return EX11 }
|
||||
func (x xhostOp) Type() Enablement { return EX11 }
|
||||
|
||||
func (x XHostOp) apply(*I) error {
|
||||
msg.Verbosef("inserting entry %s to X11", x)
|
||||
func (x xhostOp) apply(sys *I) error {
|
||||
sys.verbosef("inserting entry %s to X11", x)
|
||||
return newOpError("xhost",
|
||||
xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), false)
|
||||
sys.xcbChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), false)
|
||||
}
|
||||
|
||||
func (x XHostOp) revert(_ *I, ec *Criteria) error {
|
||||
func (x xhostOp) revert(sys *I, ec *Criteria) error {
|
||||
if ec.hasType(x.Type()) {
|
||||
msg.Verbosef("deleting entry %s from X11", x)
|
||||
sys.verbosef("deleting entry %s from X11", x)
|
||||
return newOpError("xhost",
|
||||
xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), false)
|
||||
sys.xcbChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), true)
|
||||
} else {
|
||||
msg.Verbosef("skipping entry %s in X11", x)
|
||||
sys.verbosef("skipping entry %s in X11", x)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (x XHostOp) Is(o Op) bool { target, ok := o.(XHostOp); return ok && x == target }
|
||||
func (x XHostOp) Path() string { return string(x) }
|
||||
func (x XHostOp) String() string { return string("SI:localuser:" + x) }
|
||||
func (x xhostOp) Is(o Op) bool { target, ok := o.(xhostOp); return ok && x == target }
|
||||
func (x xhostOp) Path() string { return "/tmp/.X11-unix" }
|
||||
func (x xhostOp) String() string { return string("SI:localuser:" + x) }
|
||||
|
||||
+49
-26
@@ -2,33 +2,56 @@ package system
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/system/internal/xcb"
|
||||
)
|
||||
|
||||
func TestChangeHosts(t *testing.T) {
|
||||
testCases := []string{"chronos", "keyring", "cat", "kbd", "yonah"}
|
||||
for _, tc := range testCases {
|
||||
t.Run("append ChangeHosts operation for "+tc, func(t *testing.T) {
|
||||
sys := New(t.Context(), 150)
|
||||
sys.ChangeHosts(tc)
|
||||
(&tcOp{EX11, tc}).test(t, sys.ops, []Op{
|
||||
XHostOp(tc),
|
||||
}, "ChangeHosts")
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestXHostOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"xcbChangeHosts revert", 0xbeef, EX11, xhostOp("chronos"), []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(1)),
|
||||
}, &OpError{Op: "xhost", Err: stub.UniqueError(1)}, nil, nil},
|
||||
|
||||
func TestXHost_String(t *testing.T) {
|
||||
testCases := []struct {
|
||||
username string
|
||||
want string
|
||||
}{
|
||||
{"chronos", "SI:localuser:chronos"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.want, func(t *testing.T) {
|
||||
if got := XHostOp(tc.username).String(); got != tc.want {
|
||||
t.Errorf("String() = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
{"xcbChangeHosts revert", 0xbeef, EX11, xhostOp("chronos"), []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"deleting entry %s from X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeDelete), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(0)),
|
||||
}, &OpError{Op: "xhost", Err: stub.UniqueError(0), Revert: true}},
|
||||
|
||||
{"success skip", 0xbeef, 0, xhostOp("chronos"), []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"skipping entry %s in X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
{"success", 0xbeef, EX11, xhostOp("chronos"), []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil),
|
||||
}, nil, []stub.Call{
|
||||
call("verbosef", stub.ExpectArgs{"deleting entry %s from X11", []any{xhostOp("chronos")}}, nil, nil),
|
||||
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeDelete), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil),
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, "ChangeHosts", []opsBuilderTestCase{
|
||||
{"xhost", 0xcafebabe, func(_ *testing.T, sys *I) {
|
||||
sys.ChangeHosts("chronos")
|
||||
}, []Op{
|
||||
xhostOp("chronos"),
|
||||
}, stub.Expect{}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"differs", xhostOp("kbd"), xhostOp("chronos"), false},
|
||||
{"equals", xhostOp("chronos"), xhostOp("chronos"), true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"xhost", xhostOp("chronos"), EX11, "/tmp/.X11-unix", "SI:localuser:chronos"},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -112,7 +112,31 @@
|
||||
};
|
||||
};
|
||||
|
||||
commonPaths = [
|
||||
{
|
||||
type = "bind";
|
||||
src = "/var/tmp";
|
||||
write = true;
|
||||
}
|
||||
];
|
||||
|
||||
apps = {
|
||||
"cat.gensokyo.extern.bash.linger-timeout" = {
|
||||
name = "hakurei-check-linger-timeout";
|
||||
identity = 9999;
|
||||
share = pkgs.bash;
|
||||
packages = [ pkgs.bash ];
|
||||
command = ''
|
||||
sleep infinity & disown
|
||||
exit
|
||||
'';
|
||||
wait_delay = 1;
|
||||
enablements = {
|
||||
wayland = false;
|
||||
pulse = false;
|
||||
};
|
||||
};
|
||||
|
||||
"cat.gensokyo.extern.foot.noEnablements" = {
|
||||
name = "ne-foot";
|
||||
identity = 1;
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
@@ -52,7 +53,31 @@ func (t *T) MustCheckFile(wantFilePath string) {
|
||||
t.MustCheck(want)
|
||||
}
|
||||
|
||||
func mustAbs(s string) string {
|
||||
if !path.IsAbs(s) {
|
||||
fatalf("[FAIL] %q is not absolute", s)
|
||||
panic("unreachable")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *T) MustCheck(want *TestCase) {
|
||||
checkWritableDirPaths := []string{
|
||||
"/dev/shm",
|
||||
"/tmp",
|
||||
os.Getenv("XDG_RUNTIME_DIR"),
|
||||
}
|
||||
for _, a := range checkWritableDirPaths {
|
||||
pathname := path.Join(mustAbs(a), ".hakurei-check")
|
||||
if err := os.WriteFile(pathname, make([]byte, 1<<8), 0600); err != nil {
|
||||
fatalf("[FAIL] %s", err)
|
||||
} else if err = os.Remove(pathname); err != nil {
|
||||
fatalf("[FAIL] %s", err)
|
||||
} else {
|
||||
printf("[ OK ] %s is writable", a)
|
||||
}
|
||||
}
|
||||
|
||||
if want.Env != nil {
|
||||
var (
|
||||
fail bool
|
||||
|
||||
@@ -58,12 +58,26 @@ let
|
||||
packages = [ ];
|
||||
path = "${testProgram}/bin/hakurei-test";
|
||||
args = [
|
||||
"test"
|
||||
"hakurei-test"
|
||||
"-p"
|
||||
"/var/tmp/.hakurei-check-ok.${toString identity}"
|
||||
"-t"
|
||||
(toString (builtins.toFile "hakurei-${tc.name}-want.json" (builtins.toJSON tc.want)))
|
||||
"-s"
|
||||
tc.expectedFilter.${system}
|
||||
];
|
||||
|
||||
extraPaths =
|
||||
if tc.useCommonPaths then
|
||||
[ ]
|
||||
else
|
||||
[
|
||||
{
|
||||
type = "bind";
|
||||
src = "/var/tmp";
|
||||
write = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
testCaseName = name: "cat.gensokyo.hakurei.test." + name;
|
||||
|
||||
@@ -169,6 +169,7 @@ in
|
||||
} null;
|
||||
usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
|
||||
var = fs "800001c0" {
|
||||
tmp = fs "801001ff" null null;
|
||||
lib = fs "800001c0" {
|
||||
hakurei = fs "800001c0" {
|
||||
u0 = fs "800001c0" {
|
||||
@@ -213,9 +214,10 @@ in
|
||||
(ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
|
||||
(ent "/" "/dev" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev" "tmpfs" "tmpfs" ignore)
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev" "tmpfs" "tmpfs" "rw")
|
||||
(ent "/" ignore ignore ignore ignore ignore) # order not deterministic
|
||||
(ent "/" ignore ignore ignore ignore ignore)
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000004,gid=1000004")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
|
||||
@@ -225,6 +227,7 @@ in
|
||||
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
|
||||
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
|
||||
|
||||
@@ -192,6 +192,7 @@ in
|
||||
tmp = fs "800001f8" { } null;
|
||||
usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
|
||||
var = fs "800001c0" {
|
||||
tmp = fs "801001ff" null null;
|
||||
lib = fs "800001c0" {
|
||||
hakurei = fs "800001c0" {
|
||||
u0 = fs "800001c0" {
|
||||
@@ -243,6 +244,7 @@ in
|
||||
(ent "/tty" "/dev/tty" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
|
||||
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000003,gid=1000003")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
|
||||
@@ -252,12 +254,12 @@ in
|
||||
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
|
||||
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a3" "/var/lib/hakurei/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000003,gid=1000003")
|
||||
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
|
||||
(ent "/tmp/hakurei.0/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
|
||||
@@ -180,12 +180,12 @@
|
||||
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
|
||||
(ent ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
|
||||
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000000,gid=1000000")
|
||||
(ent "/kvm" "/dev/kvm" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/" "/run/nscd" "ro,nosuid,nodev,relatime" "tmpfs" "readonly" "ro,mode=755,uid=1000000,gid=1000000")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=1000000,gid=1000000")
|
||||
(ent "/" "/run/dbus" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=1000000,gid=1000000")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000000,gid=1000000")
|
||||
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000000,gid=1000000")
|
||||
(ent "/tmp/hakurei.0/runtime/0" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/tmpdir/0" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
|
||||
@@ -190,6 +190,7 @@ in
|
||||
tmp = fs "800001f8" { } null;
|
||||
usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
|
||||
var = fs "800001c0" {
|
||||
tmp = fs "801001ff" null null;
|
||||
lib = fs "800001c0" {
|
||||
hakurei = fs "800001c0" {
|
||||
u0 = fs "800001c0" {
|
||||
@@ -241,6 +242,7 @@ in
|
||||
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
|
||||
(ent ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
|
||||
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000005,gid=1000005")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
|
||||
@@ -250,9 +252,9 @@ in
|
||||
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a5" "/var/lib/hakurei/u0/a5" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000005,gid=1000005")
|
||||
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000005,gid=1000005")
|
||||
(ent "/tmp/hakurei.0/runtime/5" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/tmpdir/5" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
|
||||
@@ -189,6 +189,7 @@ in
|
||||
tmp = fs "800001f8" { } null;
|
||||
usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
|
||||
var = fs "800001c0" {
|
||||
tmp = fs "801001ff" null null;
|
||||
lib = fs "800001c0" {
|
||||
hakurei = fs "800001c0" {
|
||||
u0 = fs "800001c0" {
|
||||
@@ -239,6 +240,7 @@ in
|
||||
(ent "/tty" "/dev/tty" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
|
||||
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000001,gid=1000001")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
|
||||
@@ -248,9 +250,9 @@ in
|
||||
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a1" "/var/lib/hakurei/u0/a1" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000001,gid=1000001")
|
||||
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000001,gid=1000001")
|
||||
(ent "/tmp/hakurei.0/runtime/1" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/tmpdir/1" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
|
||||
@@ -196,6 +196,7 @@ in
|
||||
} null;
|
||||
usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
|
||||
var = fs "800001c0" {
|
||||
tmp = fs "801001ff" null null;
|
||||
lib = fs "800001c0" {
|
||||
hakurei = fs "800001c0" {
|
||||
u0 = fs "800001c0" {
|
||||
@@ -248,6 +249,7 @@ in
|
||||
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
|
||||
(ent ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
|
||||
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000002,gid=1000002")
|
||||
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
|
||||
@@ -257,12 +259,12 @@ in
|
||||
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
|
||||
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
|
||||
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
|
||||
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,uuid=on,userxattr")
|
||||
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/var/lib/hakurei/u0/a2" "/var/lib/hakurei/u0/a2" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000002,gid=1000002")
|
||||
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000002,gid=1000002")
|
||||
(ent "/tmp/hakurei.0/runtime/2" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/hakurei.0/tmpdir/2" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
|
||||
@@ -31,6 +31,7 @@ in
|
||||
# For checking pd outcome:
|
||||
(pkgs.writeShellScriptBin "check-sandbox-pd" ''
|
||||
hakurei -v run hakurei-test \
|
||||
-p "/var/tmp/.hakurei-check-ok.0" \
|
||||
-t ${toString (builtins.toFile "hakurei-pd-want.json" (builtins.toJSON testCases.pd.want))} \
|
||||
-s ${testCases.pd.expectedFilter.${pkgs.system}} "$@"
|
||||
'')
|
||||
@@ -77,6 +78,11 @@ in
|
||||
};
|
||||
|
||||
commonPaths = [
|
||||
{
|
||||
type = "bind";
|
||||
src = "/var/tmp";
|
||||
write = true;
|
||||
}
|
||||
{
|
||||
type = "bind";
|
||||
src = "/var/cache";
|
||||
|
||||
@@ -26,7 +26,7 @@ def swaymsg(command: str = "", succeed=True, type="command"):
|
||||
|
||||
|
||||
def check_filter(check_offset, name, pname):
|
||||
pid = int(machine.wait_until_succeeds(f"pgrep -U {1000000+check_offset} -x {pname}", timeout=15))
|
||||
pid = int(machine.wait_until_succeeds(f"pgrep -U {1000000+check_offset} -x {pname}", timeout=60))
|
||||
hash = machine.succeed(f"sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 check-sandbox-{name} hash")
|
||||
print(machine.succeed(f"hakurei-test -s {hash} filter {pid}"))
|
||||
|
||||
@@ -60,7 +60,7 @@ check_offset = 0
|
||||
def check_sandbox(name):
|
||||
global check_offset
|
||||
swaymsg(f"exec script /dev/null -E always -qec check-sandbox-{name}")
|
||||
machine.wait_for_file(f"/tmp/hakurei.0/tmpdir/{check_offset}/sandbox-ok", timeout=15)
|
||||
machine.wait_for_file(f"/var/tmp/.hakurei-check-ok.{check_offset}", timeout=60)
|
||||
check_filter(check_offset, name, "hakurei-test")
|
||||
check_offset += 1
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
flagTestCase string
|
||||
flagBpfHash string
|
||||
flagMarkerPath string
|
||||
flagTestCase string
|
||||
flagBpfHash string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&flagMarkerPath, "p", "/tmp/sandbox-ok", "Pathname of completion marker")
|
||||
flag.StringVar(&flagTestCase, "t", "", "Nix store path to test case file")
|
||||
flag.StringVar(&flagBpfHash, "s", "", "String representation of expected bpf sha512 hash")
|
||||
}
|
||||
@@ -37,10 +39,10 @@ func main() {
|
||||
go func() { <-s; log.Println("exiting on signal (likely from verifier)"); os.Exit(0) }()
|
||||
|
||||
(&sandbox.T{FS: os.DirFS("/")}).MustCheckFile(flagTestCase)
|
||||
if _, err := os.Create("/tmp/sandbox-ok"); err != nil {
|
||||
if _, err := os.Create(flagMarkerPath); err != nil {
|
||||
log.Fatalf("cannot create success marker: %v", err)
|
||||
}
|
||||
log.Println("blocking for seccomp check")
|
||||
log.Printf("blocking for seccomp check (%s)", flagMarkerPath)
|
||||
select {}
|
||||
return
|
||||
}
|
||||
|
||||
+12
-5
@@ -8,9 +8,7 @@ NODE_GROUPS = ["nodes", "floating_nodes"]
|
||||
def swaymsg(command: str = "", succeed=True, type="command"):
|
||||
assert command != "" or type != "command", "Must specify command or type"
|
||||
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
|
||||
with machine.nested(
|
||||
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
|
||||
):
|
||||
with machine.nested(f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)):
|
||||
ret = (machine.succeed if succeed else machine.execute)(
|
||||
f"su - alice -c {shell}"
|
||||
)
|
||||
@@ -99,12 +97,21 @@ print(denyOutputVerbose)
|
||||
# Fail direct hsu call:
|
||||
print(machine.fail("sudo -u alice -i hsu"))
|
||||
|
||||
# Verify PrintBaseError behaviour:
|
||||
# Verify hsu fault behaviour:
|
||||
if denyOutput != "hsu: uid 1001 is not in the hsurc file\n":
|
||||
raise Exception(f"unexpected deny output:\n{denyOutput}")
|
||||
if denyOutputVerbose != "hsu: uid 1001 is not in the hsurc file\nhakurei: *cannot obtain uid from setuid wrapper: permission denied\n":
|
||||
if denyOutputVerbose != "hsu: uid 1001 is not in the hsurc file\nhakurei: *cannot retrieve user id from setuid wrapper: current user is not in the hsurc file\n":
|
||||
raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
|
||||
|
||||
# Verify timeout behaviour:
|
||||
machine.succeed('sudo -u alice -i hakurei-check-linger-timeout > /var/tmp/linger-stdout 2> /var/tmp/linger-stderr')
|
||||
linger_stdout = machine.succeed("cat /var/tmp/linger-stdout")
|
||||
linger_stderr = machine.succeed("cat /var/tmp/linger-stderr")
|
||||
if linger_stdout != "":
|
||||
raise Exception(f"unexpected stdout: {linger_stdout}")
|
||||
if linger_stderr != "init: timeout exceeded waiting for lingering processes\n":
|
||||
raise Exception(f"unexpected stderr: {linger_stderr}")
|
||||
|
||||
check_offset = 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user