Compare commits
76 Commits
| 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 | |||
| ecaf43358d | |||
| 197fa65b8f | |||
| e81a45e849 | |||
| 3920acf8c2 | |||
| 19630a9593 | |||
| 4051577d6b | |||
| ddfb865e2d | |||
| 024d2ff782 | |||
| 6f719bc3c1 | |||
| 1b5d20a39b | |||
| 49600a6f46 | |||
| b489a3bba1 | |||
| 780e3e5465 | |||
| 712cfc06d7 | |||
| f5abce9df5 | |||
| ddb003e39b | |||
| b12c290f12 | |||
| 0122593312 | |||
| 6aa431d57a | |||
| 08eeafe817 | |||
| d7c7c69a13 | |||
| 50972096cd | |||
| 905b9f9785 | |||
| 1c7e634f09 | |||
| 8d472ebf2b | |||
| 4da6463135 | |||
| eb3385d490 | |||
| b8669338da | |||
| f24dd4ab8c | |||
| a462341a0a | |||
| 84ad9791e2 | |||
| b14690aa77 | |||
| d0b6852cd7 | |||
| da0459aca1 | |||
| 1be8de6f5c | |||
| 0f41d96671 | |||
| 92f510a647 | |||
| acb6931f3e |
@ -6,11 +6,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/command"
|
"hakurei.app/command"
|
||||||
@ -24,7 +22,7 @@ import (
|
|||||||
"hakurei.app/system/dbus"
|
"hakurei.app/system/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildCommand(out io.Writer) command.Command {
|
func buildCommand(ctx context.Context, out io.Writer) command.Command {
|
||||||
var (
|
var (
|
||||||
flagVerbose bool
|
flagVerbose bool
|
||||||
flagJSON bool
|
flagJSON bool
|
||||||
@ -44,35 +42,35 @@ func buildCommand(out io.Writer) command.Command {
|
|||||||
config := tryPath(args[0])
|
config := tryPath(args[0])
|
||||||
config.Args = append(config.Args, args[1:]...)
|
config.Args = append(config.Args, args[1:]...)
|
||||||
|
|
||||||
runApp(config)
|
app.Main(ctx, config)
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
})
|
})
|
||||||
|
|
||||||
{
|
{
|
||||||
var (
|
var (
|
||||||
dbusConfigSession string
|
flagDBusConfigSession string
|
||||||
dbusConfigSystem string
|
flagDBusConfigSystem string
|
||||||
mpris bool
|
flagDBusMpris bool
|
||||||
dbusVerbose bool
|
flagDBusVerbose bool
|
||||||
|
|
||||||
fid string
|
flagID string
|
||||||
aid int
|
flagIdentity int
|
||||||
groups command.RepeatableFlag
|
flagGroups command.RepeatableFlag
|
||||||
homeDir string
|
flagHomeDir string
|
||||||
userName 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 {
|
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
|
||||||
// initialise config from flags
|
// initialise config from flags
|
||||||
config := &hst.Config{
|
config := &hst.Config{
|
||||||
ID: fid,
|
ID: flagID,
|
||||||
Args: args,
|
Args: args,
|
||||||
}
|
}
|
||||||
|
|
||||||
if aid < 0 || aid > 9999 {
|
if flagIdentity < 0 || flagIdentity > 9999 {
|
||||||
log.Fatalf("aid %d out of range", aid)
|
log.Fatalf("identity %d out of range", flagIdentity)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve home/username from os when flag is unset
|
// resolve home/username from os when flag is unset
|
||||||
@ -80,14 +78,7 @@ func buildCommand(out io.Writer) command.Command {
|
|||||||
passwd *user.User
|
passwd *user.User
|
||||||
passwdOnce sync.Once
|
passwdOnce sync.Once
|
||||||
passwdFunc = func() {
|
passwdFunc = func() {
|
||||||
var us string
|
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustID(), flagIdentity))
|
||||||
if uid, err := std.Uid(aid); err != nil {
|
|
||||||
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
|
|
||||||
os.Exit(1)
|
|
||||||
} else {
|
|
||||||
us = strconv.Itoa(uid)
|
|
||||||
}
|
|
||||||
|
|
||||||
if u, err := user.LookupId(us); err != nil {
|
if u, err := user.LookupId(us); err != nil {
|
||||||
hlog.Verbosef("cannot look up uid %s", us)
|
hlog.Verbosef("cannot look up uid %s", us)
|
||||||
passwd = &user.User{
|
passwd = &user.User{
|
||||||
@ -103,21 +94,21 @@ func buildCommand(out io.Writer) command.Command {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if homeDir == "os" {
|
if flagHomeDir == "os" {
|
||||||
passwdOnce.Do(passwdFunc)
|
passwdOnce.Do(passwdFunc)
|
||||||
homeDir = passwd.HomeDir
|
flagHomeDir = passwd.HomeDir
|
||||||
}
|
}
|
||||||
|
|
||||||
if userName == "chronos" {
|
if flagUserName == "chronos" {
|
||||||
passwdOnce.Do(passwdFunc)
|
passwdOnce.Do(passwdFunc)
|
||||||
userName = passwd.Username
|
flagUserName = passwd.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Identity = aid
|
config.Identity = flagIdentity
|
||||||
config.Groups = groups
|
config.Groups = flagGroups
|
||||||
config.Username = userName
|
config.Username = flagUserName
|
||||||
|
|
||||||
if a, err := container.NewAbs(homeDir); err != nil {
|
if a, err := container.NewAbs(flagHomeDir); err != nil {
|
||||||
log.Fatal(err.Error())
|
log.Fatal(err.Error())
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
@ -125,105 +116,114 @@ func buildCommand(out io.Writer) command.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var e system.Enablement
|
var e system.Enablement
|
||||||
if wayland {
|
if flagWayland {
|
||||||
e |= system.EWayland
|
e |= system.EWayland
|
||||||
}
|
}
|
||||||
if x11 {
|
if flagX11 {
|
||||||
e |= system.EX11
|
e |= system.EX11
|
||||||
}
|
}
|
||||||
if dBus {
|
if flagDBus {
|
||||||
e |= system.EDBus
|
e |= system.EDBus
|
||||||
}
|
}
|
||||||
if pulse {
|
if flagPulse {
|
||||||
e |= system.EPulse
|
e |= system.EPulse
|
||||||
}
|
}
|
||||||
config.Enablements = hst.NewEnablements(e)
|
config.Enablements = hst.NewEnablements(e)
|
||||||
|
|
||||||
// parse D-Bus config file from flags if applicable
|
// parse D-Bus config file from flags if applicable
|
||||||
if dBus {
|
if flagDBus {
|
||||||
if dbusConfigSession == "builtin" {
|
if flagDBusConfigSession == "builtin" {
|
||||||
config.SessionBus = dbus.NewConfig(fid, true, mpris)
|
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
|
||||||
} else {
|
} else {
|
||||||
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
|
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSession); err != nil {
|
||||||
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
|
log.Fatalf("cannot load session bus proxy config from %q: %s", flagDBusConfigSession, err)
|
||||||
} else {
|
} else {
|
||||||
config.SessionBus = conf
|
config.SessionBus = conf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// system bus proxy is optional
|
// system bus proxy is optional
|
||||||
if dbusConfigSystem != "nil" {
|
if flagDBusConfigSystem != "nil" {
|
||||||
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
|
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSystem); err != nil {
|
||||||
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
|
log.Fatalf("cannot load system bus proxy config from %q: %s", flagDBusConfigSystem, err)
|
||||||
} else {
|
} else {
|
||||||
config.SystemBus = conf
|
config.SystemBus = conf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// override log from configuration
|
// override log from configuration
|
||||||
if dbusVerbose {
|
if flagDBusVerbose {
|
||||||
config.SessionBus.Log = true
|
if config.SessionBus != nil {
|
||||||
config.SystemBus.Log = true
|
config.SessionBus.Log = true
|
||||||
|
}
|
||||||
|
if config.SystemBus != nil {
|
||||||
|
config.SystemBus.Log = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoke app
|
app.Main(ctx, config)
|
||||||
runApp(config)
|
|
||||||
panic("unreachable")
|
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").
|
"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").
|
"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").
|
"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").
|
"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").
|
"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").
|
"Application identity").
|
||||||
Flag(nil, "g", &groups,
|
Flag(nil, "g", &flagGroups,
|
||||||
"Groups inherited by all container processes").
|
"Groups inherited by all container processes").
|
||||||
Flag(&homeDir, "d", command.StringFlag("os"),
|
Flag(&flagHomeDir, "d", command.StringFlag("os"),
|
||||||
"Container home directory").
|
"Container home directory").
|
||||||
Flag(&userName, "u", command.StringFlag("chronos"),
|
Flag(&flagUserName, "u", command.StringFlag("chronos"),
|
||||||
"Passwd user name within sandbox").
|
"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").
|
"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").
|
"Enable direct connection to X11").
|
||||||
Flag(&dBus, "dbus", command.BoolFlag(false),
|
Flag(&flagDBus, "dbus", command.BoolFlag(false),
|
||||||
"Enable proxied connection to D-Bus").
|
"Enable proxied connection to D-Bus").
|
||||||
Flag(&pulse, "pulse", command.BoolFlag(false),
|
Flag(&flagPulse, "pulse", command.BoolFlag(false),
|
||||||
"Enable direct connection to PulseAudio")
|
"Enable direct connection to PulseAudio")
|
||||||
}
|
}
|
||||||
|
|
||||||
var showFlagShort bool
|
{
|
||||||
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
|
var flagShort bool
|
||||||
switch len(args) {
|
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
|
||||||
case 0: // system
|
switch len(args) {
|
||||||
printShowSystem(os.Stdout, showFlagShort, flagJSON)
|
case 0: // system
|
||||||
|
printShowSystem(os.Stdout, flagShort, flagJSON)
|
||||||
|
|
||||||
case 1: // instance
|
case 1: // instance
|
||||||
name := args[0]
|
name := args[0]
|
||||||
config, entry := tryShort(name)
|
config, entry := tryShort(name)
|
||||||
if config == nil {
|
if config == nil {
|
||||||
config = tryPath(name)
|
config = tryPath(name)
|
||||||
|
}
|
||||||
|
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON)
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Fatal("show requires 1 argument")
|
||||||
}
|
}
|
||||||
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")
|
var flagShort bool
|
||||||
}
|
c.NewCommand("ps", "List active instances", func(args []string) error {
|
||||||
return errSuccess
|
var sc hst.Paths
|
||||||
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
|
app.CopyPaths(&sc, new(app.Hsu).MustID())
|
||||||
|
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sc.RunDirPath.String()), flagShort, flagJSON)
|
||||||
var psFlagShort bool
|
return errSuccess
|
||||||
c.NewCommand("ps", "List active instances", func(args []string) error {
|
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||||
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), psFlagShort, flagJSON)
|
}
|
||||||
return errSuccess
|
|
||||||
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
|
|
||||||
|
|
||||||
c.Command("version", "Display version information", func(args []string) error {
|
c.Command("version", "Display version information", func(args []string) error {
|
||||||
fmt.Println(internal.Version())
|
fmt.Println(internal.Version())
|
||||||
@ -247,20 +247,3 @@ func buildCommand(out io.Writer) command.Command {
|
|||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func runApp(config *hst.Config) {
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(),
|
|
||||||
syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
defer stop() // unreachable
|
|
||||||
a := app.MustNew(ctx, std)
|
|
||||||
|
|
||||||
rs := new(app.RunState)
|
|
||||||
if sa, err := a.Seal(config); err != nil {
|
|
||||||
hlog.PrintBaseError(err, "cannot seal app:")
|
|
||||||
internal.Exit(1)
|
|
||||||
} else {
|
|
||||||
internal.Exit(app.PrintRunStateErr(rs, sa.Run(rs)))
|
|
||||||
}
|
|
||||||
|
|
||||||
*(*int)(nil) = 0 // not reached
|
|
||||||
}
|
|
||||||
|
|||||||
@ -68,7 +68,7 @@ Flags:
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
out := new(bytes.Buffer)
|
out := new(bytes.Buffer)
|
||||||
c := buildCommand(out)
|
c := buildCommand(t.Context(), out)
|
||||||
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
|
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
|
||||||
t.Errorf("Parse: error = %v; want %v",
|
t.Errorf("Parse: error = %v; want %v",
|
||||||
err, command.ErrHelp)
|
err, command.ErrHelp)
|
||||||
|
|||||||
@ -4,15 +4,17 @@ package main
|
|||||||
//go:generate cp ../../LICENSE .
|
//go:generate cp ../../LICENSE .
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
"hakurei.app/internal"
|
"hakurei.app/internal"
|
||||||
"hakurei.app/internal/hlog"
|
"hakurei.app/internal/hlog"
|
||||||
"hakurei.app/internal/sys"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -24,8 +26,6 @@ var (
|
|||||||
|
|
||||||
func init() { hlog.Prepare("hakurei") }
|
func init() { hlog.Prepare("hakurei") }
|
||||||
|
|
||||||
var std sys.State = new(sys.Std)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
||||||
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
|
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
|
||||||
@ -44,7 +44,11 @@ func main() {
|
|||||||
log.Fatal("this program must not run as root")
|
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)
|
hlog.Verbosef("command returned %v", err)
|
||||||
if errors.Is(err, errSuccess) {
|
if errors.Is(err, errSuccess) {
|
||||||
hlog.BeforeExit()
|
hlog.BeforeExit()
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/internal/app"
|
||||||
"hakurei.app/internal/app/state"
|
"hakurei.app/internal/app/state"
|
||||||
"hakurei.app/internal/hlog"
|
"hakurei.app/internal/hlog"
|
||||||
)
|
)
|
||||||
@ -87,7 +88,9 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
|
|||||||
if likePrefix && len(name) >= 8 {
|
if likePrefix && len(name) >= 8 {
|
||||||
hlog.Verbose("argument looks like prefix")
|
hlog.Verbose("argument looks like prefix")
|
||||||
|
|
||||||
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 {
|
if entries, err := state.Join(s); err != nil {
|
||||||
log.Printf("cannot join store: %v", err)
|
log.Printf("cannot join store: %v", err)
|
||||||
// drop to fetch from file
|
// drop to fetch from file
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -13,8 +12,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/internal/app"
|
||||||
"hakurei.app/internal/app/state"
|
"hakurei.app/internal/app/state"
|
||||||
"hakurei.app/internal/hlog"
|
|
||||||
"hakurei.app/system/dbus"
|
"hakurei.app/system/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,15 +21,8 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
|
|||||||
t := newPrinter(output)
|
t := newPrinter(output)
|
||||||
defer t.MustFlush()
|
defer t.MustFlush()
|
||||||
|
|
||||||
info := &hst.Info{Paths: std.Paths()}
|
info := &hst.Info{User: new(app.Hsu).MustID()}
|
||||||
|
app.CopyPaths(&info.Paths, info.User)
|
||||||
// get hid by querying uid of identity 0
|
|
||||||
if uid, err := std.Uid(0); err != nil {
|
|
||||||
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
|
|
||||||
os.Exit(1)
|
|
||||||
} else {
|
|
||||||
info.User = (uid / 10000) - 100
|
|
||||||
}
|
|
||||||
|
|
||||||
if flagJSON {
|
if flagJSON {
|
||||||
printJSON(output, short, info)
|
printJSON(output, short, info)
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
hsuConfFile = "/etc/hsurc"
|
hsuConfFile = "/etc/hsurc"
|
||||||
envShim = "HAKUREI_SHIM"
|
envShim = "HAKUREI_SHIM"
|
||||||
envAID = "HAKUREI_APP_ID"
|
envIdentity = "HAKUREI_IDENTITY"
|
||||||
envGroups = "HAKUREI_GROUPS"
|
envGroups = "HAKUREI_GROUPS"
|
||||||
|
|
||||||
PR_SET_NO_NEW_PRIVS = 0x26
|
PR_SET_NO_NEW_PRIVS = 0x26
|
||||||
@ -48,8 +48,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// uid = 1000000 +
|
// uid = 1000000 +
|
||||||
// fid * 10000 +
|
// id * 10000 +
|
||||||
// aid
|
// identity
|
||||||
uid := 1000000
|
uid := 1000000
|
||||||
|
|
||||||
// refuse to run if hsurc is not protected correctly
|
// refuse to run if hsurc is not protected correctly
|
||||||
@ -62,29 +62,25 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// authenticate before accepting user input
|
// authenticate before accepting user input
|
||||||
|
var id int
|
||||||
if f, err := os.Open(hsuConfFile); err != nil {
|
if f, err := os.Open(hsuConfFile); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
} else if 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)
|
log.Fatalf("uid %d is not in the hsurc file", puid)
|
||||||
} else {
|
} else {
|
||||||
uid += fid * 10000
|
id = v
|
||||||
}
|
if err = f.Close(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// allowed aid range 0 to 9999
|
uid += id * 10000
|
||||||
if as, ok := os.LookupEnv(envAID); !ok {
|
|
||||||
log.Fatal("HAKUREI_APP_ID not set")
|
|
||||||
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
|
|
||||||
log.Fatal("invalid aid")
|
|
||||||
} else {
|
|
||||||
uid += aid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pass through setup fd to shim
|
// pass through setup fd to shim
|
||||||
var shimSetupFd string
|
var shimSetupFd string
|
||||||
if s, ok := os.LookupEnv(envShim); !ok {
|
if s, ok := os.LookupEnv(envShim); !ok {
|
||||||
// hakurei requests target uid
|
// hakurei requests hsurc user id
|
||||||
// print resolved uid and exit
|
fmt.Print(id)
|
||||||
fmt.Print(uid)
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
|
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
|
||||||
log.Fatal("HAKUREI_SHIM holds an invalid value")
|
log.Fatal("HAKUREI_SHIM holds an invalid value")
|
||||||
@ -92,6 +88,15 @@ func main() {
|
|||||||
shimSetupFd = s
|
shimSetupFd = s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allowed identity range 0 to 9999
|
||||||
|
if as, ok := os.LookupEnv(envIdentity); !ok {
|
||||||
|
log.Fatal("HAKUREI_IDENTITY not set")
|
||||||
|
} else if identity, err := parseUint32Fast(as); err != nil || identity < 0 || identity > 9999 {
|
||||||
|
log.Fatal("invalid identity")
|
||||||
|
} else {
|
||||||
|
uid += identity
|
||||||
|
}
|
||||||
|
|
||||||
// supplementary groups
|
// supplementary groups
|
||||||
var suppGroups, suppCurrent []int
|
var suppGroups, suppCurrent []int
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package container
|
|||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { gob.Register(new(AutoEtcOp)) }
|
func init() { gob.Register(new(AutoEtcOp)) }
|
||||||
@ -24,7 +23,7 @@ func (e *AutoEtcOp) Valid() bool { return e != ni
|
|||||||
func (e *AutoEtcOp) early(*setupState, syscallDispatcher) error { return nil }
|
func (e *AutoEtcOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||||
func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
||||||
if state.nonrepeatable&nrAutoEtc != 0 {
|
if state.nonrepeatable&nrAutoEtc != 0 {
|
||||||
return msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
|
return OpRepeatError("autoetc")
|
||||||
}
|
}
|
||||||
state.nonrepeatable |= nrAutoEtc
|
state.nonrepeatable |= nrAutoEtc
|
||||||
|
|
||||||
@ -32,10 +31,10 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
rel := e.hostRel() + "/"
|
rel := e.hostRel() + "/"
|
||||||
|
|
||||||
if err := k.mkdirAll(target, 0755); err != nil {
|
if err := k.mkdirAll(target, 0755); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
if d, err := k.readdir(toSysroot(e.hostPath().String())); err != nil {
|
if d, err := k.readdir(toSysroot(e.hostPath().String())); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else {
|
} else {
|
||||||
for _, ent := range d {
|
for _, ent := range d {
|
||||||
n := ent.Name()
|
n := ent.Name()
|
||||||
@ -44,12 +43,12 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
|
|
||||||
case "mtab":
|
case "mtab":
|
||||||
if err = k.symlink(FHSProc+"mounts", target+n); err != nil {
|
if err = k.symlink(FHSProc+"mounts", target+n); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if err = k.symlink(rel+n, target+n); err != nil {
|
if err = k.symlink(rel+n, target+n); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,5 +64,5 @@ func (e *AutoEtcOp) Is(op Op) bool {
|
|||||||
ve, ok := op.(*AutoEtcOp)
|
ve, ok := op.(*AutoEtcOp)
|
||||||
return ok && e.Valid() && ve.Valid() && *e == *ve
|
return ok && e.Valid() && ve.Valid() && *e == *ve
|
||||||
}
|
}
|
||||||
func (*AutoEtcOp) prefix() string { return "setting up" }
|
func (*AutoEtcOp) prefix() (string, bool) { return "setting up", true }
|
||||||
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
||||||
|
|||||||
@ -2,14 +2,15 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAutoEtcOp(t *testing.T) {
|
func TestAutoEtcOp(t *testing.T) {
|
||||||
t.Run("nonrepeatable", func(t *testing.T) {
|
t.Run("nonrepeatable", func(t *testing.T) {
|
||||||
wantErr := msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
|
wantErr := OpRepeatError("autoetc")
|
||||||
if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) {
|
if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) {
|
||||||
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
||||||
}
|
}
|
||||||
@ -18,22 +19,22 @@ func TestAutoEtcOp(t *testing.T) {
|
|||||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||||
{"mkdirAll", new(Params), &AutoEtcOp{
|
{"mkdirAll", new(Params), &AutoEtcOp{
|
||||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, errUnique},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, stub.UniqueError(3)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(3)},
|
||||||
|
|
||||||
{"readdir", new(Params), &AutoEtcOp{
|
{"readdir", new(Params), &AutoEtcOp{
|
||||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), errUnique},
|
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), stub.UniqueError(2)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(2)},
|
||||||
|
|
||||||
{"symlink", new(Params), &AutoEtcOp{
|
{"symlink", new(Params), &AutoEtcOp{
|
||||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
||||||
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
||||||
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
||||||
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
||||||
@ -41,15 +42,15 @@ func TestAutoEtcOp(t *testing.T) {
|
|||||||
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
||||||
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
||||||
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
||||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
|
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, errUnique},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, stub.UniqueError(1)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(1)},
|
||||||
|
|
||||||
{"symlink mtab", new(Params), &AutoEtcOp{
|
{"symlink mtab", new(Params), &AutoEtcOp{
|
||||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
||||||
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
||||||
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
||||||
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
||||||
@ -57,41 +58,41 @@ func TestAutoEtcOp(t *testing.T) {
|
|||||||
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
||||||
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
||||||
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
||||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
|
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, errUnique},
|
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, stub.UniqueError(0)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
{"success nested", new(Params), &AutoEtcOp{
|
{"success nested", new(Params), &AutoEtcOp{
|
||||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
||||||
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
||||||
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
||||||
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
||||||
@ -99,79 +100,79 @@ func TestAutoEtcOp(t *testing.T) {
|
|||||||
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
||||||
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
||||||
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
||||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
|
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
|
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"success", new(Params), &AutoEtcOp{
|
{"success", new(Params), &AutoEtcOp{
|
||||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(
|
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(
|
||||||
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
||||||
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
||||||
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
||||||
@ -179,72 +180,72 @@ func TestAutoEtcOp(t *testing.T) {
|
|||||||
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
||||||
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
||||||
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
||||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
|
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
|
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil),
|
||||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil},
|
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package container
|
|||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { gob.Register(new(AutoRootOp)) }
|
func init() { gob.Register(new(AutoRootOp)) }
|
||||||
@ -23,19 +22,20 @@ type AutoRootOp struct {
|
|||||||
// obtained during early;
|
// obtained during early;
|
||||||
// these wrap the underlying Op because BindMountOp is relatively complex,
|
// these wrap the underlying Op because BindMountOp is relatively complex,
|
||||||
// so duplicating that code would be unwise
|
// so duplicating that code would be unwise
|
||||||
resolved []Op
|
resolved []*BindMountOp
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil }
|
func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil }
|
||||||
|
|
||||||
func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
|
func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
|
||||||
if d, err := k.readdir(r.Host.String()); err != nil {
|
if d, err := k.readdir(r.Host.String()); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else {
|
} else {
|
||||||
r.resolved = make([]Op, 0, len(d))
|
r.resolved = make([]*BindMountOp, 0, len(d))
|
||||||
for _, ent := range d {
|
for _, ent := range d {
|
||||||
name := ent.Name()
|
name := ent.Name()
|
||||||
if IsAutoRootBindable(name) {
|
if IsAutoRootBindable(name) {
|
||||||
|
// careful: the Valid method is skipped, make sure this is always valid
|
||||||
op := &BindMountOp{
|
op := &BindMountOp{
|
||||||
Source: r.Host.Append(name),
|
Source: r.Host.Append(name),
|
||||||
Target: AbsFHSRoot.Append(name),
|
Target: AbsFHSRoot.Append(name),
|
||||||
@ -53,12 +53,12 @@ func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
|
|||||||
|
|
||||||
func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
|
func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
|
||||||
if state.nonrepeatable&nrAutoRoot != 0 {
|
if state.nonrepeatable&nrAutoRoot != 0 {
|
||||||
return msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
|
return OpRepeatError("autoroot")
|
||||||
}
|
}
|
||||||
state.nonrepeatable |= nrAutoRoot
|
state.nonrepeatable |= nrAutoRoot
|
||||||
|
|
||||||
for _, op := range r.resolved {
|
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 {
|
if err := op.apply(state, k); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -72,7 +72,7 @@ func (r *AutoRootOp) Is(op Op) bool {
|
|||||||
r.Host.Is(vr.Host) &&
|
r.Host.Is(vr.Host) &&
|
||||||
r.Flags == vr.Flags
|
r.Flags == vr.Flags
|
||||||
}
|
}
|
||||||
func (*AutoRootOp) prefix() string { return "setting up" }
|
func (*AutoRootOp) prefix() (string, bool) { return "setting up", true }
|
||||||
func (r *AutoRootOp) String() string {
|
func (r *AutoRootOp) String() string {
|
||||||
return fmt.Sprintf("auto root %q flags %#x", r.Host, r.Flags)
|
return fmt.Sprintf("auto root %q flags %#x", r.Host, r.Flags)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,14 +2,15 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAutoRootOp(t *testing.T) {
|
func TestAutoRootOp(t *testing.T) {
|
||||||
t.Run("nonrepeatable", func(t *testing.T) {
|
t.Run("nonrepeatable", func(t *testing.T) {
|
||||||
wantErr := msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
|
wantErr := OpRepeatError("autoroot")
|
||||||
if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) {
|
if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) {
|
||||||
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
||||||
}
|
}
|
||||||
@ -19,100 +20,99 @@ func TestAutoRootOp(t *testing.T) {
|
|||||||
{"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
|
{"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
Host: MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: BindWritable,
|
Flags: BindWritable,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"readdir", expectArgs{"/"}, stubDir(), errUnique},
|
call("readdir", stub.ExpectArgs{"/"}, stubDir(), stub.UniqueError(2)),
|
||||||
}, wrapErrSelf(errUnique), nil, nil},
|
}, stub.UniqueError(2), nil, nil},
|
||||||
|
|
||||||
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{
|
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
Host: MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: BindWritable,
|
Flags: BindWritable,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
|
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||||
{"evalSymlinks", expectArgs{"/bin"}, "", errUnique},
|
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "", stub.UniqueError(1)),
|
||||||
}, wrapErrSelf(errUnique), nil, nil},
|
}, stub.UniqueError(1), nil, nil},
|
||||||
|
|
||||||
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
|
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
Host: MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: BindWritable,
|
Flags: BindWritable,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
|
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||||
{"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "/usr/bin", nil),
|
||||||
{"evalSymlinks", expectArgs{"/home"}, "/home", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/home"}, "/home", nil),
|
||||||
{"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/lib64"}, "/lib64", nil),
|
||||||
{"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/lost+found"}, "/lost+found", nil),
|
||||||
{"evalSymlinks", expectArgs{"/nix"}, "/nix", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/nix"}, "/nix", nil),
|
||||||
{"evalSymlinks", expectArgs{"/root"}, "/root", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/root"}, "/root", nil),
|
||||||
{"evalSymlinks", expectArgs{"/run"}, "/run", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/run"}, "/run", nil),
|
||||||
{"evalSymlinks", expectArgs{"/srv"}, "/srv", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/srv"}, "/srv", nil),
|
||||||
{"evalSymlinks", expectArgs{"/sys"}, "/sys", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sys"}, "/sys", nil),
|
||||||
{"evalSymlinks", expectArgs{"/usr"}, "/usr", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var"}, "/var", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"verbosef", 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)),
|
||||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(false), errUnique},
|
}, stub.UniqueError(0)},
|
||||||
}, wrapErrSelf(errUnique)},
|
|
||||||
|
|
||||||
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
|
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
Host: MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: BindWritable,
|
Flags: BindWritable,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
|
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||||
{"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "/usr/bin", nil),
|
||||||
{"evalSymlinks", expectArgs{"/home"}, "/home", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/home"}, "/home", nil),
|
||||||
{"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/lib64"}, "/lib64", nil),
|
||||||
{"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/lost+found"}, "/lost+found", nil),
|
||||||
{"evalSymlinks", expectArgs{"/nix"}, "/nix", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/nix"}, "/nix", nil),
|
||||||
{"evalSymlinks", expectArgs{"/root"}, "/root", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/root"}, "/root", nil),
|
||||||
{"evalSymlinks", expectArgs{"/run"}, "/run", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/run"}, "/run", nil),
|
||||||
{"evalSymlinks", expectArgs{"/srv"}, "/srv", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/srv"}, "/srv", nil),
|
||||||
{"evalSymlinks", expectArgs{"/sys"}, "/sys", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sys"}, "/sys", nil),
|
||||||
{"evalSymlinks", expectArgs{"/usr"}, "/usr", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var"}, "/var", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/home"), MustAbs("/home"), MustAbs("/home"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/home", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lib64"), MustAbs("/lib64"), MustAbs("/lib64"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/lib64", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lost+found"), MustAbs("/lost+found"), MustAbs("/lost+found"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/lost+found", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/nix"), MustAbs("/nix"), MustAbs("/nix"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/nix", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/root"), MustAbs("/root"), MustAbs("/root"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/root", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/run"), MustAbs("/run"), MustAbs("/run"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/run", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/srv"), MustAbs("/srv"), MustAbs("/srv"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/srv", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/sys"), MustAbs("/sys"), MustAbs("/sys"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/sys", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr"), MustAbs("/usr"), MustAbs("/usr"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/usr", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var"), MustAbs("/var"), MustAbs("/var"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil},
|
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},
|
}, nil},
|
||||||
|
|
||||||
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{
|
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"),
|
Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"readdir", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
call("readdir", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
|
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/bin"}, "/var/lib/planterette/base/debian:f92c9052/usr/bin", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/bin"}, "/var/lib/planterette/base/debian:f92c9052/usr/bin", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/home"}, "/var/lib/planterette/base/debian:f92c9052/home", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/home"}, "/var/lib/planterette/base/debian:f92c9052/home", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lib64"}, "/var/lib/planterette/base/debian:f92c9052/lib64", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/lib64"}, "/var/lib/planterette/base/debian:f92c9052/lib64", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lost+found"}, "/var/lib/planterette/base/debian:f92c9052/lost+found", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/lost+found"}, "/var/lib/planterette/base/debian:f92c9052/lost+found", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/nix"}, "/var/lib/planterette/base/debian:f92c9052/nix", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/nix"}, "/var/lib/planterette/base/debian:f92c9052/nix", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/root"}, "/var/lib/planterette/base/debian:f92c9052/root", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/root"}, "/var/lib/planterette/base/debian:f92c9052/root", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/run"}, "/var/lib/planterette/base/debian:f92c9052/run", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/run"}, "/var/lib/planterette/base/debian:f92c9052/run", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/srv"}, "/var/lib/planterette/base/debian:f92c9052/srv", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/srv"}, "/var/lib/planterette/base/debian:f92c9052/srv", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/sys"}, "/var/lib/planterette/base/debian:f92c9052/sys", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/sys"}, "/var/lib/planterette/base/debian:f92c9052/sys", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr/bin"), MustAbs("/var/lib/planterette/base/debian:f92c9052/bin"), MustAbs("/bin"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/home"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/lib64"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/lost+found"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/nix"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/root"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/run"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/srv"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/sys"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/usr"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil},
|
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil),
|
||||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil},
|
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},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -140,7 +140,7 @@ func TestAutoRootOp(t *testing.T) {
|
|||||||
}, &AutoRootOp{
|
}, &AutoRootOp{
|
||||||
Host: MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: BindWritable,
|
Flags: BindWritable,
|
||||||
resolved: []Op{new(BindMountOp)},
|
resolved: []*BindMountOp{new(BindMountOp)},
|
||||||
}, true},
|
}, true},
|
||||||
|
|
||||||
{"flags differs", &AutoRootOp{
|
{"flags differs", &AutoRootOp{
|
||||||
|
|||||||
@ -64,7 +64,7 @@ type (
|
|||||||
Args []string
|
Args []string
|
||||||
// Deliver SIGINT to the initial process on context cancellation.
|
// Deliver SIGINT to the initial process on context cancellation.
|
||||||
ForwardCancel bool
|
ForwardCancel bool
|
||||||
// time to wait for linger processes after death of initial process
|
// Time to wait for processes lingering after the initial process terminates.
|
||||||
AdoptWaitDelay time.Duration
|
AdoptWaitDelay time.Duration
|
||||||
|
|
||||||
// Mapped Uid in user namespace.
|
// Mapped Uid in user namespace.
|
||||||
@ -99,17 +99,66 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start starts the container init. The init process blocks until Serve is called.
|
// A StartError contains additional information on a container startup failure.
|
||||||
func (p *Container) Start() error {
|
type StartError struct {
|
||||||
if p.cmd != nil {
|
// Fatal suggests whether this error should be considered fatal for the entire program.
|
||||||
return errors.New("container: already started")
|
Fatal bool
|
||||||
|
// Step refers to the part of the setup this error is returned from.
|
||||||
|
Step string
|
||||||
|
// Err is the underlying error.
|
||||||
|
Err error
|
||||||
|
// Origin is whether this error originated from the [Container.Start] method.
|
||||||
|
Origin bool
|
||||||
|
// Passthrough is whether the Error method is passed through to Err.
|
||||||
|
Passthrough bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *StartError) Unwrap() error { return e.Err }
|
||||||
|
func (e *StartError) Error() string {
|
||||||
|
if e.Passthrough {
|
||||||
|
return e.Err.Error()
|
||||||
}
|
}
|
||||||
if p.Ops == nil || len(*p.Ops) == 0 {
|
if e.Origin {
|
||||||
return errors.New("container: starting an empty container")
|
return e.Step
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(p.ctx)
|
{
|
||||||
p.cancel = cancel
|
var syscallError *os.SyscallError
|
||||||
|
if errors.As(e.Err, &syscallError) && syscallError != nil {
|
||||||
|
return e.Step + " " + syscallError.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Step + ": " + e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message returns a user-facing error message.
|
||||||
|
func (e *StartError) Message() string {
|
||||||
|
if e.Passthrough {
|
||||||
|
switch {
|
||||||
|
case errors.As(e.Err, new(*os.PathError)),
|
||||||
|
errors.As(e.Err, new(*os.SyscallError)):
|
||||||
|
return "cannot " + e.Err.Error()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.Origin {
|
||||||
|
return e.Step
|
||||||
|
}
|
||||||
|
return "cannot " + e.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the container init. The init process blocks until Serve is called.
|
||||||
|
func (p *Container) Start() error {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
// map to overflow id to work around ownership checks
|
// map to overflow id to work around ownership checks
|
||||||
if p.Uid < 1 {
|
if p.Uid < 1 {
|
||||||
@ -131,9 +180,17 @@ func (p *Container) Start() error {
|
|||||||
p.AdoptWaitDelay = 0
|
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.Args = []string{initName}
|
||||||
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
|
|
||||||
p.cmd.WaitDelay = p.WaitDelay
|
p.cmd.WaitDelay = p.WaitDelay
|
||||||
if p.Cancel != nil {
|
if p.Cancel != nil {
|
||||||
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
|
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
|
||||||
@ -167,8 +224,7 @@ func (p *Container) Start() error {
|
|||||||
|
|
||||||
// place setup pipe before user supplied extra files, this is later restored by init
|
// place setup pipe before user supplied extra files, this is later restored by init
|
||||||
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
|
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
|
||||||
return wrapErrSuffix(err,
|
return &StartError{true, "set up params stream", err, false, false}
|
||||||
"cannot create shim setup pipe:")
|
|
||||||
} else {
|
} else {
|
||||||
p.setup = e
|
p.setup = e
|
||||||
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
|
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
|
||||||
@ -183,8 +239,7 @@ func (p *Container) Start() error {
|
|||||||
done <- func() error { // setup depending on per-thread state must happen here
|
done <- func() error { // setup depending on per-thread state must happen here
|
||||||
// PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread
|
// PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread
|
||||||
if err := SetNoNewPrivs(); err != nil {
|
if err := SetNoNewPrivs(); err != nil {
|
||||||
return wrapErrSuffix(err,
|
return &StartError{true, "prctl(PR_SET_NO_NEW_PRIVS)", err, false, false}
|
||||||
"prctl(PR_SET_NO_NEW_PRIVS):")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// landlock: depends on per-thread state but acts on a process group
|
// landlock: depends on per-thread state but acts on a process group
|
||||||
@ -200,28 +255,24 @@ func (p *Container) Start() error {
|
|||||||
// already covered by namespaces (pid)
|
// already covered by namespaces (pid)
|
||||||
goto landlockOut
|
goto landlockOut
|
||||||
}
|
}
|
||||||
return wrapErrSuffix(err,
|
return &StartError{false, "get landlock ABI", err, false, false}
|
||||||
"landlock does not appear to be enabled:")
|
|
||||||
} else if abi < 6 {
|
} else if abi < 6 {
|
||||||
if p.HostAbstract {
|
if p.HostAbstract {
|
||||||
// see above comment
|
// see above comment
|
||||||
goto landlockOut
|
goto landlockOut
|
||||||
}
|
}
|
||||||
return msg.WrapErr(ENOSYS,
|
return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
|
||||||
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET")
|
|
||||||
} else {
|
} else {
|
||||||
msg.Verbosef("landlock abi version %d", abi)
|
msg.Verbosef("landlock abi version %d", abi)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
|
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
|
||||||
return wrapErrSuffix(err,
|
return &StartError{true, "create landlock ruleset", err, false, false}
|
||||||
"cannot create landlock ruleset:")
|
|
||||||
} else {
|
} else {
|
||||||
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
|
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
|
||||||
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
|
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
|
||||||
_ = Close(rulesetFd)
|
_ = Close(rulesetFd)
|
||||||
return wrapErrSuffix(err,
|
return &StartError{true, "enforce landlock ruleset", err, false, false}
|
||||||
"cannot enforce landlock ruleset:")
|
|
||||||
}
|
}
|
||||||
if err = Close(rulesetFd); err != nil {
|
if err = Close(rulesetFd); err != nil {
|
||||||
msg.Verbosef("cannot close landlock ruleset: %v", err)
|
msg.Verbosef("cannot close landlock ruleset: %v", err)
|
||||||
@ -234,7 +285,7 @@ func (p *Container) Start() error {
|
|||||||
|
|
||||||
msg.Verbose("starting container init")
|
msg.Verbose("starting container init")
|
||||||
if err := p.cmd.Start(); err != nil {
|
if err := p.cmd.Start(); err != nil {
|
||||||
return msg.WrapErr(err, err.Error())
|
return &StartError{false, "start container init", err, false, true}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
@ -257,7 +308,7 @@ func (p *Container) Serve() error {
|
|||||||
|
|
||||||
if p.Path == nil {
|
if p.Path == nil {
|
||||||
p.cancel()
|
p.cancel()
|
||||||
return msg.WrapErr(EINVAL, "invalid executable pathname")
|
return &StartError{false, "invalid executable pathname", EINVAL, true, false}
|
||||||
}
|
}
|
||||||
|
|
||||||
// do not transmit nil
|
// do not transmit nil
|
||||||
@ -285,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].
|
// Wait waits for the container init process to exit and releases any resources associated with the [Container].
|
||||||
func (p *Container) Wait() error {
|
func (p *Container) Wait() error {
|
||||||
if p.cmd == nil {
|
if p.cmd == nil || p.cmd.Process == nil {
|
||||||
return EINVAL
|
return EINVAL
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,6 +348,36 @@ func (p *Container) Wait() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StdinPipe calls the [exec.Cmd] method with the same name.
|
||||||
|
func (p *Container) StdinPipe() (w io.WriteCloser, err error) {
|
||||||
|
if p.Stdin != nil {
|
||||||
|
return nil, errors.New("container: Stdin already set")
|
||||||
|
}
|
||||||
|
w, err = p.cmd.StdinPipe()
|
||||||
|
p.Stdin = p.cmd.Stdin
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StdoutPipe calls the [exec.Cmd] method with the same name.
|
||||||
|
func (p *Container) StdoutPipe() (r io.ReadCloser, err error) {
|
||||||
|
if p.Stdout != nil {
|
||||||
|
return nil, errors.New("container: Stdout already set")
|
||||||
|
}
|
||||||
|
r, err = p.cmd.StdoutPipe()
|
||||||
|
p.Stdout = p.cmd.Stdout
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// StderrPipe calls the [exec.Cmd] method with the same name.
|
||||||
|
func (p *Container) StderrPipe() (r io.ReadCloser, err error) {
|
||||||
|
if p.Stderr != nil {
|
||||||
|
return nil, errors.New("container: Stderr already set")
|
||||||
|
}
|
||||||
|
r, err = p.cmd.StderrPipe()
|
||||||
|
p.Stderr = p.cmd.Stderr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Container) String() string {
|
func (p *Container) String() string {
|
||||||
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
|
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
|
||||||
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
|
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
|
||||||
@ -312,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.
|
// New returns the address to a new instance of [Container] that requires further initialisation before use.
|
||||||
func New(ctx context.Context) *Container {
|
func New(ctx context.Context) *Container {
|
||||||
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.
|
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
|
||||||
|
|||||||
@ -7,9 +7,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@ -26,6 +28,143 @@ import (
|
|||||||
"hakurei.app/ldd"
|
"hakurei.app/ldd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestStartError(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
s string
|
||||||
|
is error
|
||||||
|
isF error
|
||||||
|
msg string
|
||||||
|
}{
|
||||||
|
{"params env", &container.StartError{
|
||||||
|
Fatal: true,
|
||||||
|
Step: "set up params stream",
|
||||||
|
Err: container.ErrReceiveEnv,
|
||||||
|
},
|
||||||
|
"set up params stream: environment variable not set",
|
||||||
|
container.ErrReceiveEnv, syscall.EBADF,
|
||||||
|
"cannot set up params stream: environment variable not set"},
|
||||||
|
|
||||||
|
{"params", &container.StartError{
|
||||||
|
Fatal: true,
|
||||||
|
Step: "set up params stream",
|
||||||
|
Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF},
|
||||||
|
},
|
||||||
|
"set up params stream pipe2: bad file descriptor",
|
||||||
|
syscall.EBADF, os.ErrInvalid,
|
||||||
|
"cannot set up params stream pipe2: bad file descriptor"},
|
||||||
|
|
||||||
|
{"PR_SET_NO_NEW_PRIVS", &container.StartError{
|
||||||
|
Fatal: true,
|
||||||
|
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
|
||||||
|
Err: syscall.EPERM,
|
||||||
|
},
|
||||||
|
"prctl(PR_SET_NO_NEW_PRIVS): operation not permitted",
|
||||||
|
syscall.EPERM, syscall.EACCES,
|
||||||
|
"cannot prctl(PR_SET_NO_NEW_PRIVS): operation not permitted"},
|
||||||
|
|
||||||
|
{"landlock abi", &container.StartError{
|
||||||
|
Step: "get landlock ABI",
|
||||||
|
Err: syscall.ENOSYS,
|
||||||
|
},
|
||||||
|
"get landlock ABI: function not implemented",
|
||||||
|
syscall.ENOSYS, syscall.ENOEXEC,
|
||||||
|
"cannot get landlock ABI: function not implemented"},
|
||||||
|
|
||||||
|
{"landlock old", &container.StartError{
|
||||||
|
Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
|
||||||
|
Err: syscall.ENOSYS,
|
||||||
|
Origin: true,
|
||||||
|
},
|
||||||
|
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
|
||||||
|
syscall.ENOSYS, syscall.ENOSPC,
|
||||||
|
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"},
|
||||||
|
|
||||||
|
{"landlock create", &container.StartError{
|
||||||
|
Fatal: true,
|
||||||
|
Step: "create landlock ruleset",
|
||||||
|
Err: syscall.EBADFD,
|
||||||
|
},
|
||||||
|
"create landlock ruleset: file descriptor in bad state",
|
||||||
|
syscall.EBADFD, syscall.EBADF,
|
||||||
|
"cannot create landlock ruleset: file descriptor in bad state"},
|
||||||
|
|
||||||
|
{"landlock enforce", &container.StartError{
|
||||||
|
Fatal: true,
|
||||||
|
Step: "enforce landlock ruleset",
|
||||||
|
Err: syscall.ENOTRECOVERABLE,
|
||||||
|
},
|
||||||
|
"enforce landlock ruleset: state not recoverable",
|
||||||
|
syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT,
|
||||||
|
"cannot enforce landlock ruleset: state not recoverable"},
|
||||||
|
|
||||||
|
{"start", &container.StartError{
|
||||||
|
Step: "start container init",
|
||||||
|
Err: &os.PathError{
|
||||||
|
Op: "fork/exec",
|
||||||
|
Path: "/proc/nonexistent",
|
||||||
|
Err: syscall.ENOENT,
|
||||||
|
}, Passthrough: true,
|
||||||
|
},
|
||||||
|
"fork/exec /proc/nonexistent: no such file or directory",
|
||||||
|
syscall.ENOENT, syscall.ENOSYS,
|
||||||
|
"cannot fork/exec /proc/nonexistent: no such file or directory"},
|
||||||
|
|
||||||
|
{"start syscall", &container.StartError{
|
||||||
|
Step: "start container init",
|
||||||
|
Err: &os.SyscallError{
|
||||||
|
Syscall: "open",
|
||||||
|
Err: syscall.ENOSYS,
|
||||||
|
}, Passthrough: true,
|
||||||
|
},
|
||||||
|
"open: function not implemented",
|
||||||
|
syscall.ENOSYS, syscall.ENOENT,
|
||||||
|
"cannot open: function not implemented"},
|
||||||
|
|
||||||
|
{"start other", &container.StartError{
|
||||||
|
Step: "start container init",
|
||||||
|
Err: &net.OpError{
|
||||||
|
Op: "dial",
|
||||||
|
Net: "unix",
|
||||||
|
Err: syscall.ECONNREFUSED,
|
||||||
|
}, Passthrough: true,
|
||||||
|
},
|
||||||
|
"dial unix: connection refused",
|
||||||
|
syscall.ECONNREFUSED, syscall.ECONNABORTED,
|
||||||
|
"dial unix: connection refused"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Run("error", func(t *testing.T) {
|
||||||
|
if got := tc.err.Error(); got != tc.s {
|
||||||
|
t.Errorf("Error: %q, want %q", got, tc.s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("is", func(t *testing.T) {
|
||||||
|
if !errors.Is(tc.err, tc.is) {
|
||||||
|
t.Error("Is: unexpected false")
|
||||||
|
}
|
||||||
|
if errors.Is(tc.err, tc.isF) {
|
||||||
|
t.Errorf("Is: unexpected true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("msg", func(t *testing.T) {
|
||||||
|
if got, ok := container.GetErrorMessage(tc.err); !ok {
|
||||||
|
if tc.msg != "" {
|
||||||
|
t.Errorf("GetErrorMessage: err does not implement MessageError")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if got != tc.msg {
|
||||||
|
t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ignore = "\x00"
|
ignore = "\x00"
|
||||||
ignoreV = -1
|
ignoreV = -1
|
||||||
@ -75,7 +214,7 @@ var containerTestCases = []struct {
|
|||||||
1000, 100, nil, 0, seccomp.PresetExt},
|
1000, 100, nil, 0, seccomp.PresetExt},
|
||||||
{"custom rules", true, true, true, false,
|
{"custom rules", true, true, true, false,
|
||||||
emptyOps, emptyMnt,
|
emptyOps, emptyMnt,
|
||||||
1, 31, []seccomp.NativeRule{{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}}, 0, seccomp.PresetExt},
|
1, 31, []seccomp.NativeRule{{Syscall: seccomp.ScmpSyscall(syscall.SYS_SETUID), Errno: seccomp.ScmpErrno(syscall.EPERM)}}, 0, seccomp.PresetExt},
|
||||||
|
|
||||||
{"tmpfs", true, false, false, true,
|
{"tmpfs", true, false, false, true,
|
||||||
earlyOps(new(container.Ops).
|
earlyOps(new(container.Ops).
|
||||||
@ -217,9 +356,11 @@ func TestContainer(t *testing.T) {
|
|||||||
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
|
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
|
||||||
wantErr := context.Canceled
|
wantErr := context.Canceled
|
||||||
wantExitCode := 0
|
wantExitCode := 0
|
||||||
if err := c.Wait(); !errors.Is(err, wantErr) {
|
if err := c.Wait(); !reflect.DeepEqual(err, wantErr) {
|
||||||
container.GetOutput().PrintBaseErr(err, "wait:")
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
t.Errorf("Wait: error = %v, want %v", err, wantErr)
|
t.Error(m)
|
||||||
|
}
|
||||||
|
t.Errorf("Wait: error = %#v, want %#v", err, wantErr)
|
||||||
}
|
}
|
||||||
if ps := c.ProcessState(); ps == nil {
|
if ps := c.ProcessState(); ps == nil {
|
||||||
t.Errorf("ProcessState unexpectedly returned nil")
|
t.Errorf("ProcessState unexpectedly returned nil")
|
||||||
@ -233,7 +374,9 @@ func TestContainer(t *testing.T) {
|
|||||||
}, func(t *testing.T, c *container.Container) {
|
}, func(t *testing.T, c *container.Container) {
|
||||||
var exitError *exec.ExitError
|
var exitError *exec.ExitError
|
||||||
if err := c.Wait(); !errors.As(err, &exitError) {
|
if err := c.Wait(); !errors.As(err, &exitError) {
|
||||||
container.GetOutput().PrintBaseErr(err, "wait:")
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
|
t.Error(m)
|
||||||
|
}
|
||||||
t.Errorf("Wait: error = %v", err)
|
t.Errorf("Wait: error = %v", err)
|
||||||
}
|
}
|
||||||
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
|
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
|
||||||
@ -313,17 +456,26 @@ func TestContainer(t *testing.T) {
|
|||||||
|
|
||||||
if err := c.Start(); err != nil {
|
if err := c.Start(); err != nil {
|
||||||
_, _ = output.WriteTo(os.Stdout)
|
_, _ = output.WriteTo(os.Stdout)
|
||||||
container.GetOutput().PrintBaseErr(err, "start:")
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
t.Fatalf("cannot start container: %v", err)
|
t.Fatal(m)
|
||||||
|
} else {
|
||||||
|
t.Fatalf("cannot start container: %v", err)
|
||||||
|
}
|
||||||
} else if err = c.Serve(); err != nil {
|
} else if err = c.Serve(); err != nil {
|
||||||
_, _ = output.WriteTo(os.Stdout)
|
_, _ = output.WriteTo(os.Stdout)
|
||||||
container.GetOutput().PrintBaseErr(err, "serve:")
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
t.Errorf("cannot serve setup params: %v", err)
|
t.Error(m)
|
||||||
|
} else {
|
||||||
|
t.Errorf("cannot serve setup params: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := c.Wait(); err != nil {
|
if err := c.Wait(); err != nil {
|
||||||
_, _ = output.WriteTo(os.Stdout)
|
_, _ = output.WriteTo(os.Stdout)
|
||||||
container.GetOutput().PrintBaseErr(err, "wait:")
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
t.Fatalf("wait: %v", err)
|
t.Fatal(m)
|
||||||
|
} else {
|
||||||
|
t.Fatalf("wait: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -376,11 +528,17 @@ func testContainerCancel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := c.Start(); err != nil {
|
if err := c.Start(); err != nil {
|
||||||
container.GetOutput().PrintBaseErr(err, "start:")
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
t.Fatalf("cannot start container: %v", err)
|
t.Fatal(m)
|
||||||
|
} else {
|
||||||
|
t.Fatalf("cannot start container: %v", err)
|
||||||
|
}
|
||||||
} else if err = c.Serve(); err != nil {
|
} else if err = c.Serve(); err != nil {
|
||||||
container.GetOutput().PrintBaseErr(err, "serve:")
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
t.Errorf("cannot serve setup params: %v", err)
|
t.Error(m)
|
||||||
|
} else {
|
||||||
|
t.Errorf("cannot serve setup params: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
<-ready
|
<-ready
|
||||||
cancel()
|
cancel()
|
||||||
|
|||||||
@ -53,7 +53,7 @@ type syscallDispatcher interface {
|
|||||||
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
||||||
|
|
||||||
// bindMount provides procPaths.bindMount.
|
// bindMount provides procPaths.bindMount.
|
||||||
bindMount(source, target string, flags uintptr, eq bool) error
|
bindMount(source, target string, flags uintptr) error
|
||||||
// remount provides procPaths.remount.
|
// remount provides procPaths.remount.
|
||||||
remount(target string, flags uintptr) error
|
remount(target string, flags uintptr) error
|
||||||
// mountTmpfs provides mountTmpfs.
|
// mountTmpfs provides mountTmpfs.
|
||||||
@ -138,8 +138,6 @@ type syscallDispatcher interface {
|
|||||||
resume() bool
|
resume() bool
|
||||||
// beforeExit provides [Msg.BeforeExit].
|
// beforeExit provides [Msg.BeforeExit].
|
||||||
beforeExit()
|
beforeExit()
|
||||||
// printBaseErr provides [Msg.PrintBaseErr].
|
|
||||||
printBaseErr(err error, fallback string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// direct implements syscallDispatcher on the current kernel.
|
// direct implements syscallDispatcher on the current kernel.
|
||||||
@ -163,8 +161,8 @@ func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
|||||||
return Receive(key, e, fdp)
|
return Receive(key, e, fdp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (direct) bindMount(source, target string, flags uintptr, eq bool) error {
|
func (direct) bindMount(source, target string, flags uintptr) error {
|
||||||
return hostProc.bindMount(source, target, flags, eq)
|
return hostProc.bindMount(source, target, flags)
|
||||||
}
|
}
|
||||||
func (direct) remount(target string, flags uintptr) error {
|
func (direct) remount(target string, flags uintptr) error {
|
||||||
return hostProc.remount(target, flags)
|
return hostProc.remount(target, flags)
|
||||||
@ -225,7 +223,7 @@ func (direct) pivotRoot(newroot, putold string) (err error) {
|
|||||||
return syscall.PivotRoot(newroot, putold)
|
return syscall.PivotRoot(newroot, putold)
|
||||||
}
|
}
|
||||||
func (direct) mount(source, target, fstype string, flags uintptr, data string) (err error) {
|
func (direct) mount(source, target, fstype string, flags uintptr, data string) (err error) {
|
||||||
return syscall.Mount(source, target, fstype, flags, data)
|
return mount(source, target, fstype, flags, data)
|
||||||
}
|
}
|
||||||
func (direct) unmount(target string, flags int) (err error) {
|
func (direct) unmount(target string, flags int) (err error) {
|
||||||
return syscall.Unmount(target, flags)
|
return syscall.Unmount(target, flags)
|
||||||
@ -234,12 +232,11 @@ func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *s
|
|||||||
return syscall.Wait4(pid, wstatus, options, rusage)
|
return syscall.Wait4(pid, wstatus, options, rusage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (direct) printf(format string, v ...any) { log.Printf(format, v...) }
|
func (direct) printf(format string, v ...any) { log.Printf(format, v...) }
|
||||||
func (direct) fatal(v ...any) { log.Fatal(v...) }
|
func (direct) fatal(v ...any) { log.Fatal(v...) }
|
||||||
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }
|
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }
|
||||||
func (direct) verbose(v ...any) { msg.Verbose(v...) }
|
func (direct) verbose(v ...any) { msg.Verbose(v...) }
|
||||||
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
|
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
|
||||||
func (direct) suspend() { msg.Suspend() }
|
func (direct) suspend() { msg.Suspend() }
|
||||||
func (direct) resume() bool { return msg.Resume() }
|
func (direct) resume() bool { return msg.Resume() }
|
||||||
func (direct) beforeExit() { msg.BeforeExit() }
|
func (direct) beforeExit() { msg.BeforeExit() }
|
||||||
func (direct) printBaseErr(err error, fallback string) { msg.PrintBaseErr(err, fallback) }
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
@ -11,16 +10,14 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/container/seccomp"
|
"hakurei.app/container/seccomp"
|
||||||
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errUnique = errors.New("unique error injected by the test suite")
|
|
||||||
|
|
||||||
type opValidTestCase struct {
|
type opValidTestCase struct {
|
||||||
name string
|
name string
|
||||||
op Op
|
op Op
|
||||||
@ -28,9 +25,15 @@ type opValidTestCase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
|
func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
t.Run("valid", func(t *testing.T) {
|
t.Run("valid", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
if got := tc.op.Valid(); got != tc.want {
|
if got := tc.op.Valid(); got != tc.want {
|
||||||
t.Errorf("Valid: %v, want %v", got, tc.want)
|
t.Errorf("Valid: %v, want %v", got, tc.want)
|
||||||
}
|
}
|
||||||
@ -46,9 +49,15 @@ type opsBuilderTestCase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
|
func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
t.Run("build", func(t *testing.T) {
|
t.Run("build", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
|
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
|
||||||
t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
|
t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
|
||||||
}
|
}
|
||||||
@ -64,9 +73,15 @@ type opIsTestCase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkOpIs(t *testing.T, testCases []opIsTestCase) {
|
func checkOpIs(t *testing.T, testCases []opIsTestCase) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
t.Run("is", func(t *testing.T) {
|
t.Run("is", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
if got := tc.op.Is(tc.v); got != tc.want {
|
if got := tc.op.Is(tc.v); got != tc.want {
|
||||||
t.Errorf("Is: %v, want %v", got, tc.want)
|
t.Errorf("Is: %v, want %v", got, tc.want)
|
||||||
}
|
}
|
||||||
@ -84,16 +99,26 @@ type opMetaTestCase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
|
func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
t.Run("meta", func(t *testing.T) {
|
t.Run("meta", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
t.Run("prefix", func(t *testing.T) {
|
t.Run("prefix", func(t *testing.T) {
|
||||||
if got := tc.op.prefix(); got != tc.wantPrefix {
|
t.Helper()
|
||||||
|
|
||||||
|
if got, _ := tc.op.prefix(); got != tc.wantPrefix {
|
||||||
t.Errorf("prefix: %q, want %q", got, tc.wantPrefix)
|
t.Errorf("prefix: %q, want %q", got, tc.wantPrefix)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("string", func(t *testing.T) {
|
t.Run("string", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
if got := tc.op.String(); got != tc.wantString {
|
if got := tc.op.String(); got != tc.wantString {
|
||||||
t.Errorf("String: %s, want %s", got, tc.wantString)
|
t.Errorf("String: %s, want %s", got, tc.wantString)
|
||||||
}
|
}
|
||||||
@ -103,23 +128,36 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// call initialises a [stub.Call].
|
||||||
|
// This keeps composites analysis happy without making the test cases too bloated.
|
||||||
|
func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
|
||||||
|
return stub.Call{Name: name, Args: args, Ret: ret, Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
type simpleTestCase struct {
|
type simpleTestCase struct {
|
||||||
name string
|
name string
|
||||||
f func(k syscallDispatcher) error
|
f func(k syscallDispatcher) error
|
||||||
want [][]kexpect
|
want stub.Expect
|
||||||
wantErr error
|
wantErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
|
func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
defer handleExitStub()
|
t.Helper()
|
||||||
k := &kstub{t: t, want: tc.want, wg: new(sync.WaitGroup)}
|
|
||||||
if err := tc.f(k); !errors.Is(err, tc.wantErr) {
|
wait4signal := make(chan struct{})
|
||||||
|
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
|
||||||
|
defer stub.HandleExit(t)
|
||||||
|
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
|
||||||
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
|
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
|
||||||
}
|
}
|
||||||
k.handleIncomplete(func(k *kstub) {
|
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
|
||||||
t.Errorf("%s: %d calls, want %d (track %d)", fname, k.pos, len(k.want[k.track]), k.track)
|
t.Helper()
|
||||||
|
|
||||||
|
t.Errorf("%s: %d calls, want %d", fname, s.Pos(), s.Len())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -130,36 +168,45 @@ type opBehaviourTestCase struct {
|
|||||||
params *Params
|
params *Params
|
||||||
op Op
|
op Op
|
||||||
|
|
||||||
early []kexpect
|
early []stub.Call
|
||||||
wantErrEarly error
|
wantErrEarly error
|
||||||
|
|
||||||
apply []kexpect
|
apply []stub.Call
|
||||||
wantErrApply error
|
wantErrApply error
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
|
func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
t.Run("behaviour", func(t *testing.T) {
|
t.Run("behaviour", func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
defer handleExitStub()
|
t.Helper()
|
||||||
|
|
||||||
state := &setupState{Params: tc.params}
|
state := &setupState{Params: tc.params}
|
||||||
k := &kstub{t: t, want: [][]kexpect{slices.Concat(tc.early, []kexpect{{name: "\x00"}}, tc.apply)}, wg: new(sync.WaitGroup)}
|
k := &kstub{nil, stub.New(t,
|
||||||
|
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
|
||||||
|
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
|
||||||
|
)}
|
||||||
|
defer stub.HandleExit(t)
|
||||||
errEarly := tc.op.early(state, k)
|
errEarly := tc.op.early(state, k)
|
||||||
k.expect("\x00")
|
k.Expects(stub.CallSeparator)
|
||||||
if !errors.Is(errEarly, tc.wantErrEarly) {
|
if !reflect.DeepEqual(errEarly, tc.wantErrEarly) {
|
||||||
t.Errorf("early: error = %v, want %v", errEarly, tc.wantErrEarly)
|
t.Errorf("early: error = %v, want %v", errEarly, tc.wantErrEarly)
|
||||||
}
|
}
|
||||||
if errEarly != nil {
|
if errEarly != nil {
|
||||||
goto out
|
goto out
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tc.op.apply(state, k); !errors.Is(err, tc.wantErrApply) {
|
if err := tc.op.apply(state, k); !reflect.DeepEqual(err, tc.wantErrApply) {
|
||||||
t.Errorf("apply: error = %v, want %v", err, tc.wantErrApply)
|
t.Errorf("apply: error = %v, want %v", err, tc.wantErrApply)
|
||||||
}
|
}
|
||||||
|
|
||||||
out:
|
out:
|
||||||
k.handleIncomplete(func(k *kstub) {
|
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
|
||||||
count := k.pos - 1 // separator
|
count := k.Pos() - 1 // separator
|
||||||
if count < len(tc.early) {
|
if count < len(tc.early) {
|
||||||
t.Errorf("early: %d calls, want %d", count, len(tc.early))
|
t.Errorf("early: %d calls, want %d", count, len(tc.early))
|
||||||
} else {
|
} else {
|
||||||
@ -226,8 +273,6 @@ func (writeErrOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
|
|||||||
func (writeErrOsFile) Read([]byte) (int, error) { panic("unreachable") }
|
func (writeErrOsFile) Read([]byte) (int, error) { panic("unreachable") }
|
||||||
func (writeErrOsFile) Close() error { panic("unreachable") }
|
func (writeErrOsFile) Close() error { panic("unreachable") }
|
||||||
|
|
||||||
type expectArgs = [5]any
|
|
||||||
|
|
||||||
type isDirFi bool
|
type isDirFi bool
|
||||||
|
|
||||||
func (isDirFi) Name() string { panic("unreachable") }
|
func (isDirFi) Name() string { panic("unreachable") }
|
||||||
@ -252,184 +297,94 @@ func (nameDentry) IsDir() bool { panic("unreachable") }
|
|||||||
func (nameDentry) Type() fs.FileMode { panic("unreachable") }
|
func (nameDentry) Type() fs.FileMode { panic("unreachable") }
|
||||||
func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") }
|
func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") }
|
||||||
|
|
||||||
type kexpect struct {
|
const (
|
||||||
name string
|
// magicWait4Signal must be used in a single pair of signal and wait4 calls across two goroutines
|
||||||
args expectArgs
|
// originating from the same toplevel kstub.
|
||||||
ret any
|
// To enable this behaviour this value must be the last element of the args field in the wait4 call
|
||||||
err error
|
// and the ret value of the signal call.
|
||||||
}
|
magicWait4Signal = 0xdef
|
||||||
|
)
|
||||||
func (k *kexpect) error(ok ...bool) error {
|
|
||||||
if !slices.Contains(ok, false) {
|
|
||||||
return k.err
|
|
||||||
}
|
|
||||||
return syscall.ENOTRECOVERABLE
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleExitStub() {
|
|
||||||
r := recover()
|
|
||||||
if r == 0xdeadbeef {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r != nil {
|
|
||||||
panic(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type kstub struct {
|
type kstub struct {
|
||||||
t *testing.T
|
wait4signal chan struct{}
|
||||||
|
*stub.Stub[syscallDispatcher]
|
||||||
want [][]kexpect
|
|
||||||
// pos is the current position in want[track].
|
|
||||||
pos int
|
|
||||||
// track is the current active want.
|
|
||||||
track int
|
|
||||||
// sub stores addresses of kstub created by new.
|
|
||||||
sub []*kstub
|
|
||||||
// wg waits for all descendants to complete.
|
|
||||||
wg *sync.WaitGroup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleIncomplete calls f on an incomplete k and all its descendants.
|
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
|
||||||
func (k *kstub) handleIncomplete(f func(k *kstub)) {
|
|
||||||
k.wg.Wait()
|
|
||||||
|
|
||||||
if k.want != nil && len(k.want[k.track]) != k.pos {
|
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") }
|
||||||
f(k)
|
|
||||||
}
|
|
||||||
for _, sk := range k.sub {
|
|
||||||
sk.handleIncomplete(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// expect checks name and returns the current kexpect and advances pos.
|
|
||||||
func (k *kstub) expect(name string) (expect *kexpect) {
|
|
||||||
if len(k.want[k.track]) == k.pos {
|
|
||||||
k.t.Fatal("expect: want too short")
|
|
||||||
}
|
|
||||||
expect = &k.want[k.track][k.pos]
|
|
||||||
if name != expect.name {
|
|
||||||
if expect.name == "\x00" {
|
|
||||||
k.t.Fatalf("expect: func = %s, separator overrun", name)
|
|
||||||
}
|
|
||||||
if name == "\x00" {
|
|
||||||
k.t.Fatalf("expect: separator, want %s", expect.name)
|
|
||||||
}
|
|
||||||
k.t.Fatalf("expect: func = %s, want %s", name, expect.name)
|
|
||||||
}
|
|
||||||
k.pos++
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkArg checks an argument comparable with the == operator. Avoid using this with pointers.
|
|
||||||
func checkArg[T comparable](k *kstub, arg string, got T, n int) bool {
|
|
||||||
if k.pos == 0 {
|
|
||||||
panic("invalid call to checkArg")
|
|
||||||
}
|
|
||||||
expect := k.want[k.track][k.pos-1]
|
|
||||||
want, ok := expect.args[n].(T)
|
|
||||||
if !ok || got != want {
|
|
||||||
k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkArgReflect checks an argument of any type.
|
|
||||||
func checkArgReflect(k *kstub, arg string, got any, n int) bool {
|
|
||||||
if k.pos == 0 {
|
|
||||||
panic("invalid call to checkArgReflect")
|
|
||||||
}
|
|
||||||
expect := k.want[k.track][k.pos-1]
|
|
||||||
want := expect.args[n]
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *kstub) new(f func(k syscallDispatcher)) {
|
|
||||||
k.expect("new")
|
|
||||||
if len(k.want) <= k.track+1 {
|
|
||||||
k.t.Fatalf("new: track overrun")
|
|
||||||
}
|
|
||||||
sk := &kstub{t: k.t, want: k.want, track: len(k.sub) + 1, wg: k.wg}
|
|
||||||
k.sub = append(k.sub, sk)
|
|
||||||
k.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer k.wg.Done()
|
|
||||||
defer handleExitStub()
|
|
||||||
f(sk)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *kstub) lockOSThread() { k.expect("lockOSThread") }
|
|
||||||
|
|
||||||
func (k *kstub) setPtracer(pid uintptr) error {
|
func (k *kstub) setPtracer(pid uintptr) error {
|
||||||
return k.expect("setPtracer").error(
|
k.Helper()
|
||||||
checkArg(k, "pid", pid, 0))
|
return k.Expects("setPtracer").Error(
|
||||||
|
stub.CheckArg(k.Stub, "pid", pid, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) setDumpable(dumpable uintptr) error {
|
func (k *kstub) setDumpable(dumpable uintptr) error {
|
||||||
return k.expect("setDumpable").error(
|
k.Helper()
|
||||||
checkArg(k, "dumpable", dumpable, 0))
|
return k.Expects("setDumpable").Error(
|
||||||
|
stub.CheckArg(k.Stub, "dumpable", dumpable, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) setNoNewPrivs() error { return k.expect("setNoNewPrivs").err }
|
func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err }
|
||||||
func (k *kstub) lastcap() uintptr { return k.expect("lastcap").ret.(uintptr) }
|
func (k *kstub) lastcap() uintptr { k.Helper(); return k.Expects("lastcap").Ret.(uintptr) }
|
||||||
|
|
||||||
func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
|
func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
|
||||||
return k.expect("capset").error(
|
k.Helper()
|
||||||
checkArgReflect(k, "hdrp", hdrp, 0),
|
return k.Expects("capset").Error(
|
||||||
checkArgReflect(k, "datap", datap, 1))
|
stub.CheckArgReflect(k.Stub, "hdrp", hdrp, 0),
|
||||||
|
stub.CheckArgReflect(k.Stub, "datap", datap, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) capBoundingSetDrop(cap uintptr) error {
|
func (k *kstub) capBoundingSetDrop(cap uintptr) error {
|
||||||
return k.expect("capBoundingSetDrop").error(
|
k.Helper()
|
||||||
checkArg(k, "cap", cap, 0))
|
return k.Expects("capBoundingSetDrop").Error(
|
||||||
|
stub.CheckArg(k.Stub, "cap", cap, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) capAmbientClearAll() error { return k.expect("capAmbientClearAll").err }
|
func (k *kstub) capAmbientClearAll() error { k.Helper(); return k.Expects("capAmbientClearAll").Err }
|
||||||
|
|
||||||
func (k *kstub) capAmbientRaise(cap uintptr) error {
|
func (k *kstub) capAmbientRaise(cap uintptr) error {
|
||||||
return k.expect("capAmbientRaise").error(
|
k.Helper()
|
||||||
checkArg(k, "cap", cap, 0))
|
return k.Expects("capAmbientRaise").Error(
|
||||||
|
stub.CheckArg(k.Stub, "cap", cap, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) isatty(fd int) bool {
|
func (k *kstub) isatty(fd int) bool {
|
||||||
expect := k.expect("isatty")
|
k.Helper()
|
||||||
if !checkArg(k, "fd", fd, 0) {
|
expect := k.Expects("isatty")
|
||||||
k.t.FailNow()
|
if !stub.CheckArg(k.Stub, "fd", fd, 0) {
|
||||||
|
k.FailNow()
|
||||||
}
|
}
|
||||||
return expect.ret.(bool)
|
return expect.Ret.(bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
||||||
expect := k.expect("receive")
|
k.Helper()
|
||||||
|
expect := k.Expects("receive")
|
||||||
|
|
||||||
var closed bool
|
var closed bool
|
||||||
closeFunc = func() error {
|
closeFunc = func() error {
|
||||||
if closed {
|
if closed {
|
||||||
k.t.Error("closeFunc called more than once")
|
k.Error("closeFunc called more than once")
|
||||||
return os.ErrClosed
|
return os.ErrClosed
|
||||||
}
|
}
|
||||||
closed = true
|
closed = true
|
||||||
|
|
||||||
if expect.ret != nil {
|
if expect.Ret != nil {
|
||||||
// use return stored in kexpect for closeFunc instead
|
// use return stored in kexpect for closeFunc instead
|
||||||
return expect.ret.(error)
|
return expect.Ret.(error)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err = expect.error(
|
err = expect.Error(
|
||||||
checkArg(k, "key", key, 0),
|
stub.CheckArg(k.Stub, "key", key, 0),
|
||||||
checkArgReflect(k, "e", e, 1),
|
stub.CheckArgReflect(k.Stub, "e", e, 1),
|
||||||
checkArgReflect(k, "fdp", fdp, 2))
|
stub.CheckArgReflect(k.Stub, "fdp", fdp, 2))
|
||||||
|
|
||||||
// 3 is unused so stores params
|
// 3 is unused so stores params
|
||||||
if expect.args[3] != nil {
|
if expect.Args[3] != nil {
|
||||||
if v, ok := expect.args[3].(*initParams); ok && v != nil {
|
if v, ok := expect.Args[3].(*initParams); ok && v != nil {
|
||||||
if p, ok0 := e.(*initParams); ok0 && p != nil {
|
if p, ok0 := e.(*initParams); ok0 && p != nil {
|
||||||
*p = *v
|
*p = *v
|
||||||
}
|
}
|
||||||
@ -437,8 +392,8 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4 is unused so stores fd
|
// 4 is unused so stores fd
|
||||||
if expect.args[4] != nil {
|
if expect.Args[4] != nil {
|
||||||
if v, ok := expect.args[4].(uintptr); ok && v >= 3 {
|
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
|
||||||
if fdp != nil {
|
if fdp != nil {
|
||||||
*fdp = v
|
*fdp = v
|
||||||
}
|
}
|
||||||
@ -448,247 +403,291 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) bindMount(source, target string, flags uintptr, eq bool) error {
|
func (k *kstub) bindMount(source, target string, flags uintptr) error {
|
||||||
return k.expect("bindMount").error(
|
k.Helper()
|
||||||
checkArg(k, "source", source, 0),
|
return k.Expects("bindMount").Error(
|
||||||
checkArg(k, "target", target, 1),
|
stub.CheckArg(k.Stub, "source", source, 0),
|
||||||
checkArg(k, "flags", flags, 2),
|
stub.CheckArg(k.Stub, "target", target, 1),
|
||||||
checkArg(k, "eq", eq, 3))
|
stub.CheckArg(k.Stub, "flags", flags, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) remount(target string, flags uintptr) error {
|
func (k *kstub) remount(target string, flags uintptr) error {
|
||||||
return k.expect("remount").error(
|
k.Helper()
|
||||||
checkArg(k, "target", target, 0),
|
return k.Expects("remount").Error(
|
||||||
checkArg(k, "flags", flags, 1))
|
stub.CheckArg(k.Stub, "target", target, 0),
|
||||||
|
stub.CheckArg(k.Stub, "flags", flags, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
|
func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
|
||||||
return k.expect("mountTmpfs").error(
|
k.Helper()
|
||||||
checkArg(k, "fsname", fsname, 0),
|
return k.Expects("mountTmpfs").Error(
|
||||||
checkArg(k, "target", target, 1),
|
stub.CheckArg(k.Stub, "fsname", fsname, 0),
|
||||||
checkArg(k, "flags", flags, 2),
|
stub.CheckArg(k.Stub, "target", target, 1),
|
||||||
checkArg(k, "size", size, 3),
|
stub.CheckArg(k.Stub, "flags", flags, 2),
|
||||||
checkArg(k, "perm", perm, 4))
|
stub.CheckArg(k.Stub, "size", size, 3),
|
||||||
|
stub.CheckArg(k.Stub, "perm", perm, 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
|
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||||
|
k.Helper()
|
||||||
return k.expect("ensureFile").error(
|
return k.Expects("ensureFile").Error(
|
||||||
checkArg(k, "name", name, 0),
|
stub.CheckArg(k.Stub, "name", name, 0),
|
||||||
checkArg(k, "perm", perm, 1),
|
stub.CheckArg(k.Stub, "perm", perm, 1),
|
||||||
checkArg(k, "pperm", pperm, 2))
|
stub.CheckArg(k.Stub, "pperm", pperm, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
|
func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
|
||||||
return k.expect("seccompLoad").error(
|
k.Helper()
|
||||||
checkArgReflect(k, "rules", rules, 0),
|
return k.Expects("seccompLoad").Error(
|
||||||
checkArg(k, "flags", flags, 1))
|
stub.CheckArgReflect(k.Stub, "rules", rules, 0),
|
||||||
|
stub.CheckArg(k.Stub, "flags", flags, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
|
func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
|
||||||
expect := k.expect("notify")
|
k.Helper()
|
||||||
if c == nil || expect.error(
|
expect := k.Expects("notify")
|
||||||
checkArgReflect(k, "sig", sig, 1)) != nil {
|
if c == nil || expect.Error(
|
||||||
k.t.FailNow()
|
stub.CheckArgReflect(k.Stub, "sig", sig, 1)) != nil {
|
||||||
|
k.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// export channel for external instrumentation
|
// export channel for external instrumentation
|
||||||
if chanf, ok := expect.args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
|
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
|
||||||
chanf(c)
|
chanf(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) start(c *exec.Cmd) error {
|
func (k *kstub) start(c *exec.Cmd) error {
|
||||||
expect := k.expect("start")
|
k.Helper()
|
||||||
err := expect.error(
|
expect := k.Expects("start")
|
||||||
checkArg(k, "c.Path", c.Path, 0),
|
err := expect.Error(
|
||||||
checkArgReflect(k, "c.Args", c.Args, 1),
|
stub.CheckArg(k.Stub, "c.Path", c.Path, 0),
|
||||||
checkArgReflect(k, "c.Env", c.Env, 2),
|
stub.CheckArgReflect(k.Stub, "c.Args", c.Args, 1),
|
||||||
checkArg(k, "c.Dir", c.Dir, 3))
|
stub.CheckArgReflect(k.Stub, "c.Env", c.Env, 2),
|
||||||
|
stub.CheckArg(k.Stub, "c.Dir", c.Dir, 3))
|
||||||
|
|
||||||
if process, ok := expect.ret.(*os.Process); ok && process != nil {
|
if process, ok := expect.Ret.(*os.Process); ok && process != nil {
|
||||||
c.Process = process
|
c.Process = process
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) signal(c *exec.Cmd, sig os.Signal) error {
|
func (k *kstub) signal(c *exec.Cmd, sig os.Signal) error {
|
||||||
return k.expect("signal").error(
|
k.Helper()
|
||||||
checkArg(k, "c.Path", c.Path, 0),
|
expect := k.Expects("signal")
|
||||||
checkArgReflect(k, "c.Args", c.Args, 1),
|
if v, ok := expect.Ret.(int); ok && v == magicWait4Signal {
|
||||||
checkArgReflect(k, "c.Env", c.Env, 2),
|
if k.wait4signal == nil {
|
||||||
checkArg(k, "c.Dir", c.Dir, 3),
|
panic("kstub not initialised for wait4 simulation")
|
||||||
checkArg(k, "sig", sig, 4))
|
}
|
||||||
|
defer func() { close(k.wait4signal) }()
|
||||||
|
}
|
||||||
|
return expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "c.Path", c.Path, 0),
|
||||||
|
stub.CheckArgReflect(k.Stub, "c.Args", c.Args, 1),
|
||||||
|
stub.CheckArgReflect(k.Stub, "c.Env", c.Env, 2),
|
||||||
|
stub.CheckArg(k.Stub, "c.Dir", c.Dir, 3),
|
||||||
|
stub.CheckArg(k.Stub, "sig", sig, 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) evalSymlinks(path string) (string, error) {
|
func (k *kstub) evalSymlinks(path string) (string, error) {
|
||||||
expect := k.expect("evalSymlinks")
|
k.Helper()
|
||||||
return expect.ret.(string), expect.error(
|
expect := k.Expects("evalSymlinks")
|
||||||
checkArg(k, "path", path, 0))
|
return expect.Ret.(string), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "path", path, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) exit(code int) {
|
func (k *kstub) exit(code int) {
|
||||||
k.expect("exit")
|
k.Helper()
|
||||||
if !checkArg(k, "code", code, 0) {
|
k.Expects("exit")
|
||||||
k.t.FailNow()
|
if !stub.CheckArg(k.Stub, "code", code, 0) {
|
||||||
|
k.FailNow()
|
||||||
}
|
}
|
||||||
panic(0xdeadbeef)
|
panic(stub.PanicExit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) getpid() int { return k.expect("getpid").ret.(int) }
|
func (k *kstub) getpid() int { k.Helper(); return k.Expects("getpid").Ret.(int) }
|
||||||
|
|
||||||
func (k *kstub) stat(name string) (os.FileInfo, error) {
|
func (k *kstub) stat(name string) (os.FileInfo, error) {
|
||||||
expect := k.expect("stat")
|
k.Helper()
|
||||||
return expect.ret.(os.FileInfo), expect.error(
|
expect := k.Expects("stat")
|
||||||
checkArg(k, "name", name, 0))
|
return expect.Ret.(os.FileInfo), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) mkdir(name string, perm os.FileMode) error {
|
func (k *kstub) mkdir(name string, perm os.FileMode) error {
|
||||||
return k.expect("mkdir").error(
|
k.Helper()
|
||||||
checkArg(k, "name", name, 0),
|
return k.Expects("mkdir").Error(
|
||||||
checkArg(k, "perm", perm, 1))
|
stub.CheckArg(k.Stub, "name", name, 0),
|
||||||
|
stub.CheckArg(k.Stub, "perm", perm, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) mkdirTemp(dir, pattern string) (string, error) {
|
func (k *kstub) mkdirTemp(dir, pattern string) (string, error) {
|
||||||
expect := k.expect("mkdirTemp")
|
k.Helper()
|
||||||
return expect.ret.(string), expect.error(
|
expect := k.Expects("mkdirTemp")
|
||||||
checkArg(k, "dir", dir, 0),
|
return expect.Ret.(string), expect.Error(
|
||||||
checkArg(k, "pattern", pattern, 1))
|
stub.CheckArg(k.Stub, "dir", dir, 0),
|
||||||
|
stub.CheckArg(k.Stub, "pattern", pattern, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) mkdirAll(path string, perm os.FileMode) error {
|
func (k *kstub) mkdirAll(path string, perm os.FileMode) error {
|
||||||
return k.expect("mkdirAll").error(
|
k.Helper()
|
||||||
checkArg(k, "path", path, 0),
|
return k.Expects("mkdirAll").Error(
|
||||||
checkArg(k, "perm", perm, 1))
|
stub.CheckArg(k.Stub, "path", path, 0),
|
||||||
|
stub.CheckArg(k.Stub, "perm", perm, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) readdir(name string) ([]os.DirEntry, error) {
|
func (k *kstub) readdir(name string) ([]os.DirEntry, error) {
|
||||||
expect := k.expect("readdir")
|
k.Helper()
|
||||||
return expect.ret.([]os.DirEntry), expect.error(
|
expect := k.Expects("readdir")
|
||||||
checkArg(k, "name", name, 0))
|
return expect.Ret.([]os.DirEntry), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) openNew(name string) (osFile, error) {
|
func (k *kstub) openNew(name string) (osFile, error) {
|
||||||
expect := k.expect("openNew")
|
k.Helper()
|
||||||
return expect.ret.(osFile), expect.error(
|
expect := k.Expects("openNew")
|
||||||
checkArg(k, "name", name, 0))
|
return expect.Ret.(osFile), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) writeFile(name string, data []byte, perm os.FileMode) error {
|
func (k *kstub) writeFile(name string, data []byte, perm os.FileMode) error {
|
||||||
return k.expect("writeFile").error(
|
k.Helper()
|
||||||
checkArg(k, "name", name, 0),
|
return k.Expects("writeFile").Error(
|
||||||
checkArgReflect(k, "data", data, 1),
|
stub.CheckArg(k.Stub, "name", name, 0),
|
||||||
checkArg(k, "perm", perm, 2))
|
stub.CheckArgReflect(k.Stub, "data", data, 1),
|
||||||
|
stub.CheckArg(k.Stub, "perm", perm, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) createTemp(dir, pattern string) (osFile, error) {
|
func (k *kstub) createTemp(dir, pattern string) (osFile, error) {
|
||||||
expect := k.expect("createTemp")
|
k.Helper()
|
||||||
return expect.ret.(osFile), expect.error(
|
expect := k.Expects("createTemp")
|
||||||
checkArg(k, "dir", dir, 0),
|
return expect.Ret.(osFile), expect.Error(
|
||||||
checkArg(k, "pattern", pattern, 1))
|
stub.CheckArg(k.Stub, "dir", dir, 0),
|
||||||
|
stub.CheckArg(k.Stub, "pattern", pattern, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) remove(name string) error {
|
func (k *kstub) remove(name string) error {
|
||||||
return k.expect("remove").error(
|
k.Helper()
|
||||||
checkArg(k, "name", name, 0))
|
return k.Expects("remove").Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) newFile(fd uintptr, name string) *os.File {
|
func (k *kstub) newFile(fd uintptr, name string) *os.File {
|
||||||
expect := k.expect("newFile")
|
k.Helper()
|
||||||
if expect.error(
|
expect := k.Expects("newFile")
|
||||||
checkArg(k, "fd", fd, 0),
|
if expect.Error(
|
||||||
checkArg(k, "name", name, 1)) != nil {
|
stub.CheckArg(k.Stub, "fd", fd, 0),
|
||||||
k.t.FailNow()
|
stub.CheckArg(k.Stub, "name", name, 1)) != nil {
|
||||||
|
k.FailNow()
|
||||||
}
|
}
|
||||||
return expect.ret.(*os.File)
|
return expect.Ret.(*os.File)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) symlink(oldname, newname string) error {
|
func (k *kstub) symlink(oldname, newname string) error {
|
||||||
return k.expect("symlink").error(
|
k.Helper()
|
||||||
checkArg(k, "oldname", oldname, 0),
|
return k.Expects("symlink").Error(
|
||||||
checkArg(k, "newname", newname, 1))
|
stub.CheckArg(k.Stub, "oldname", oldname, 0),
|
||||||
|
stub.CheckArg(k.Stub, "newname", newname, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) readlink(name string) (string, error) {
|
func (k *kstub) readlink(name string) (string, error) {
|
||||||
expect := k.expect("readlink")
|
k.Helper()
|
||||||
return expect.ret.(string), expect.error(
|
expect := k.Expects("readlink")
|
||||||
checkArg(k, "name", name, 0))
|
return expect.Ret.(string), expect.Error(
|
||||||
|
stub.CheckArg(k.Stub, "name", name, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) umask(mask int) (oldmask int) {
|
func (k *kstub) umask(mask int) (oldmask int) {
|
||||||
expect := k.expect("umask")
|
k.Helper()
|
||||||
if !checkArg(k, "mask", mask, 0) {
|
expect := k.Expects("umask")
|
||||||
k.t.FailNow()
|
if !stub.CheckArg(k.Stub, "mask", mask, 0) {
|
||||||
|
k.FailNow()
|
||||||
}
|
}
|
||||||
return expect.ret.(int)
|
return expect.Ret.(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) sethostname(p []byte) (err error) {
|
func (k *kstub) sethostname(p []byte) (err error) {
|
||||||
return k.expect("sethostname").error(
|
k.Helper()
|
||||||
checkArgReflect(k, "p", p, 0))
|
return k.Expects("sethostname").Error(
|
||||||
|
stub.CheckArgReflect(k.Stub, "p", p, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) chdir(path string) (err error) {
|
func (k *kstub) chdir(path string) (err error) {
|
||||||
return k.expect("chdir").error(
|
k.Helper()
|
||||||
checkArg(k, "path", path, 0))
|
return k.Expects("chdir").Error(
|
||||||
|
stub.CheckArg(k.Stub, "path", path, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) fchdir(fd int) (err error) {
|
func (k *kstub) fchdir(fd int) (err error) {
|
||||||
return k.expect("fchdir").error(
|
k.Helper()
|
||||||
checkArg(k, "fd", fd, 0))
|
return k.Expects("fchdir").Error(
|
||||||
|
stub.CheckArg(k.Stub, "fd", fd, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) open(path string, mode int, perm uint32) (fd int, err error) {
|
func (k *kstub) open(path string, mode int, perm uint32) (fd int, err error) {
|
||||||
expect := k.expect("open")
|
k.Helper()
|
||||||
return expect.ret.(int), expect.error(
|
expect := k.Expects("open")
|
||||||
checkArg(k, "path", path, 0),
|
return expect.Ret.(int), expect.Error(
|
||||||
checkArg(k, "mode", mode, 1),
|
stub.CheckArg(k.Stub, "path", path, 0),
|
||||||
checkArg(k, "perm", perm, 2))
|
stub.CheckArg(k.Stub, "mode", mode, 1),
|
||||||
|
stub.CheckArg(k.Stub, "perm", perm, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) close(fd int) (err error) {
|
func (k *kstub) close(fd int) (err error) {
|
||||||
return k.expect("close").error(
|
k.Helper()
|
||||||
checkArg(k, "fd", fd, 0))
|
return k.Expects("close").Error(
|
||||||
|
stub.CheckArg(k.Stub, "fd", fd, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) pivotRoot(newroot, putold string) (err error) {
|
func (k *kstub) pivotRoot(newroot, putold string) (err error) {
|
||||||
return k.expect("pivotRoot").error(
|
k.Helper()
|
||||||
checkArg(k, "newroot", newroot, 0),
|
return k.Expects("pivotRoot").Error(
|
||||||
checkArg(k, "putold", putold, 1))
|
stub.CheckArg(k.Stub, "newroot", newroot, 0),
|
||||||
|
stub.CheckArg(k.Stub, "putold", putold, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) mount(source, target, fstype string, flags uintptr, data string) (err error) {
|
func (k *kstub) mount(source, target, fstype string, flags uintptr, data string) (err error) {
|
||||||
return k.expect("mount").error(
|
k.Helper()
|
||||||
checkArg(k, "source", source, 0),
|
return k.Expects("mount").Error(
|
||||||
checkArg(k, "target", target, 1),
|
stub.CheckArg(k.Stub, "source", source, 0),
|
||||||
checkArg(k, "fstype", fstype, 2),
|
stub.CheckArg(k.Stub, "target", target, 1),
|
||||||
checkArg(k, "flags", flags, 3),
|
stub.CheckArg(k.Stub, "fstype", fstype, 2),
|
||||||
checkArg(k, "data", data, 4))
|
stub.CheckArg(k.Stub, "flags", flags, 3),
|
||||||
|
stub.CheckArg(k.Stub, "data", data, 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) unmount(target string, flags int) (err error) {
|
func (k *kstub) unmount(target string, flags int) (err error) {
|
||||||
return k.expect("unmount").error(
|
k.Helper()
|
||||||
checkArg(k, "target", target, 0),
|
return k.Expects("unmount").Error(
|
||||||
checkArg(k, "flags", flags, 1))
|
stub.CheckArg(k.Stub, "target", target, 0),
|
||||||
|
stub.CheckArg(k.Stub, "flags", flags, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) {
|
func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) {
|
||||||
expect := k.expect("wait4")
|
k.Helper()
|
||||||
// special case to prevent leaking the wait4 goroutine when testing initEntrypoint
|
expect := k.Expects("wait4")
|
||||||
if v, ok := expect.args[4].(int); ok && v == 0xdeadbeef {
|
if v, ok := expect.Args[4].(int); ok {
|
||||||
k.t.Log("terminating current goroutine as requested by kexpect")
|
switch v {
|
||||||
panic(0xdeadbeef)
|
case stub.PanicExit: // special case to prevent leaking the wait4 goroutine while testing initEntrypoint
|
||||||
|
panic(stub.PanicExit)
|
||||||
|
|
||||||
|
case magicWait4Signal: // block until corresponding signal call
|
||||||
|
if k.wait4signal == nil {
|
||||||
|
panic("kstub not initialised for wait4 simulation")
|
||||||
|
}
|
||||||
|
<-k.wait4signal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wpid = expect.ret.(int)
|
wpid = expect.Ret.(int)
|
||||||
err = expect.error(
|
err = expect.Error(
|
||||||
checkArg(k, "pid", pid, 0),
|
stub.CheckArg(k.Stub, "pid", pid, 0),
|
||||||
checkArg(k, "options", options, 2))
|
stub.CheckArg(k.Stub, "options", options, 2))
|
||||||
|
|
||||||
if wstatusV, ok := expect.args[1].(syscall.WaitStatus); wstatus != nil && ok {
|
if wstatusV, ok := expect.Args[1].(syscall.WaitStatus); wstatus != nil && ok {
|
||||||
*wstatus = wstatusV
|
*wstatus = wstatusV
|
||||||
}
|
}
|
||||||
if rusageV, ok := expect.args[3].(syscall.Rusage); rusage != nil && ok {
|
if rusageV, ok := expect.Args[3].(syscall.Rusage); rusage != nil && ok {
|
||||||
*rusage = rusageV
|
*rusage = rusageV
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -696,53 +695,50 @@ func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) printf(format string, v ...any) {
|
func (k *kstub) printf(format string, v ...any) {
|
||||||
if k.expect("printf").error(
|
k.Helper()
|
||||||
checkArg(k, "format", format, 0),
|
if k.Expects("printf").Error(
|
||||||
checkArgReflect(k, "v", v, 1)) != nil {
|
stub.CheckArg(k.Stub, "format", format, 0),
|
||||||
k.t.FailNow()
|
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
|
||||||
|
k.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) fatal(v ...any) {
|
func (k *kstub) fatal(v ...any) {
|
||||||
if k.expect("fatal").error(
|
k.Helper()
|
||||||
checkArgReflect(k, "v", v, 0)) != nil {
|
if k.Expects("fatal").Error(
|
||||||
k.t.FailNow()
|
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
|
||||||
|
k.FailNow()
|
||||||
}
|
}
|
||||||
panic(0xdeadbeef)
|
panic(stub.PanicExit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) fatalf(format string, v ...any) {
|
func (k *kstub) fatalf(format string, v ...any) {
|
||||||
if k.expect("fatalf").error(
|
k.Helper()
|
||||||
checkArg(k, "format", format, 0),
|
if k.Expects("fatalf").Error(
|
||||||
checkArgReflect(k, "v", v, 1)) != nil {
|
stub.CheckArg(k.Stub, "format", format, 0),
|
||||||
k.t.FailNow()
|
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
|
||||||
|
k.FailNow()
|
||||||
}
|
}
|
||||||
panic(0xdeadbeef)
|
panic(stub.PanicExit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) verbose(v ...any) {
|
func (k *kstub) verbose(v ...any) {
|
||||||
if k.expect("verbose").error(
|
k.Helper()
|
||||||
checkArgReflect(k, "v", v, 0)) != nil {
|
if k.Expects("verbose").Error(
|
||||||
k.t.FailNow()
|
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
|
||||||
|
k.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) verbosef(format string, v ...any) {
|
func (k *kstub) verbosef(format string, v ...any) {
|
||||||
if k.expect("verbosef").error(
|
k.Helper()
|
||||||
checkArg(k, "format", format, 0),
|
if k.Expects("verbosef").Error(
|
||||||
checkArgReflect(k, "v", v, 1)) != nil {
|
stub.CheckArg(k.Stub, "format", format, 0),
|
||||||
k.t.FailNow()
|
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
|
||||||
|
k.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) suspend() { k.expect("suspend") }
|
func (k *kstub) suspend() { k.Helper(); k.Expects("suspend") }
|
||||||
func (k *kstub) resume() bool { return k.expect("resume").ret.(bool) }
|
func (k *kstub) resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
|
||||||
func (k *kstub) beforeExit() { k.expect("beforeExit") }
|
func (k *kstub) beforeExit() { k.Helper(); k.Expects("beforeExit") }
|
||||||
|
|
||||||
func (k *kstub) printBaseErr(err error, fallback string) {
|
|
||||||
if k.expect("printBaseErr").error(
|
|
||||||
checkArgReflect(k, "err", err, 0),
|
|
||||||
checkArg(k, "fallback", fallback, 1)) != nil {
|
|
||||||
k.t.FailNow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
112
container/errors.go
Normal file
112
container/errors.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"hakurei.app/container/vfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// messageFromError returns a printable error message for a supported concrete type.
|
||||||
|
func messageFromError(err error) (string, bool) {
|
||||||
|
if m, ok := messagePrefixP[MountError]("cannot ", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
if m, ok := messagePrefixP[os.PathError]("cannot ", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
if m, ok := messagePrefixP[AbsoluteError]("", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
if m, ok := messagePrefix[OpRepeatError]("", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
if m, ok := messagePrefix[OpStateError]("", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
if m, ok := messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
if m, ok := messagePrefix[TmpfsSizeError]("", err); ok {
|
||||||
|
return m, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
return zeroString, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// messagePrefix checks and prefixes the error message of a non-pointer error.
|
||||||
|
// While this is usable for pointer errors, such use should be avoided as nil check is omitted.
|
||||||
|
func messagePrefix[T error](prefix string, err error) (string, bool) {
|
||||||
|
var targetError T
|
||||||
|
if errors.As(err, &targetError) {
|
||||||
|
return prefix + targetError.Error(), true
|
||||||
|
}
|
||||||
|
return zeroString, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// messagePrefixP checks and prefixes the error message of a pointer error.
|
||||||
|
func messagePrefixP[V any, T interface {
|
||||||
|
*V
|
||||||
|
error
|
||||||
|
}](prefix string, err error) (string, bool) {
|
||||||
|
var targetError T
|
||||||
|
if errors.As(err, &targetError) && targetError != nil {
|
||||||
|
return prefix + targetError.Error(), true
|
||||||
|
}
|
||||||
|
return zeroString, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type MountError struct {
|
||||||
|
Source, Target, Fstype string
|
||||||
|
|
||||||
|
Flags uintptr
|
||||||
|
Data string
|
||||||
|
syscall.Errno
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MountError) Unwrap() error {
|
||||||
|
if e.Errno == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.Errno
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MountError) Error() string {
|
||||||
|
if e.Flags&syscall.MS_BIND != 0 {
|
||||||
|
if e.Flags&syscall.MS_REMOUNT != 0 {
|
||||||
|
return "remount " + e.Target + ": " + e.Errno.Error()
|
||||||
|
}
|
||||||
|
return "bind " + e.Source + " on " + e.Target + ": " + e.Errno.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Fstype != FstypeNULL {
|
||||||
|
return "mount " + e.Fstype + " on " + e.Target + ": " + e.Errno.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback case: if this is reached, the conditions for it to occur should be handled above
|
||||||
|
return "mount " + e.Target + ": " + e.Errno.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback.
|
||||||
|
func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
|
||||||
|
var errno syscall.Errno
|
||||||
|
if !errors.As(err, &errno) {
|
||||||
|
return 0, &os.PathError{Op: op, Path: path, Err: err}
|
||||||
|
}
|
||||||
|
return errno, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mount wraps syscall.Mount for error handling.
|
||||||
|
func mount(source, target, fstype string, flags uintptr, data string) error {
|
||||||
|
err := syscall.Mount(source, target, fstype, flags, data)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if errno, pathError := errnoFallback("mount", target, err); pathError != nil {
|
||||||
|
return pathError
|
||||||
|
} else {
|
||||||
|
return &MountError{source, target, fstype, flags, data, errno}
|
||||||
|
}
|
||||||
|
}
|
||||||
168
container/errors_test.go
Normal file
168
container/errors_test.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
"hakurei.app/container/vfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMessageFromError(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
want string
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{"mount", &MountError{
|
||||||
|
Source: SourceTmpfsEphemeral,
|
||||||
|
Target: "/sysroot/tmp",
|
||||||
|
Fstype: FstypeTmpfs,
|
||||||
|
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
|
||||||
|
Data: zeroString,
|
||||||
|
Errno: syscall.EINVAL,
|
||||||
|
}, "cannot mount tmpfs on /sysroot/tmp: invalid argument", true},
|
||||||
|
|
||||||
|
{"path", &os.PathError{
|
||||||
|
Op: "mount",
|
||||||
|
Path: "/sysroot",
|
||||||
|
Err: stub.UniqueError(0xdeadbeef),
|
||||||
|
}, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true},
|
||||||
|
|
||||||
|
{"absolute", &AbsoluteError{"etc/mtab"},
|
||||||
|
`path "etc/mtab" is not absolute`, true},
|
||||||
|
|
||||||
|
{"repeat", OpRepeatError("autoetc"),
|
||||||
|
"autoetc is not repeatable", true},
|
||||||
|
|
||||||
|
{"state", OpStateError("overlay"),
|
||||||
|
"impossible overlay state reached", true},
|
||||||
|
|
||||||
|
{"vfs parse", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
|
||||||
|
`cannot parse mountinfo at line 3735928559: numeric field "meow" invalid syntax`, true},
|
||||||
|
|
||||||
|
{"tmpfs", TmpfsSizeError(-1),
|
||||||
|
"tmpfs size -1 out of bounds", true},
|
||||||
|
|
||||||
|
{"unsupported", stub.UniqueError(0xdeadbeef), zeroString, false},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, ok := messageFromError(tc.err)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("messageFromError: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
if ok != tc.wantOk {
|
||||||
|
t.Errorf("messageFromError: ok = %v, want %v", ok, tc.wantOk)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountError(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
errno syscall.Errno
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"bind", &MountError{
|
||||||
|
Source: "/host/nix/store",
|
||||||
|
Target: "/sysroot/nix/store",
|
||||||
|
Fstype: FstypeNULL,
|
||||||
|
Flags: syscall.MS_SILENT | syscall.MS_BIND | syscall.MS_REC,
|
||||||
|
Data: zeroString,
|
||||||
|
Errno: syscall.ENOSYS,
|
||||||
|
}, syscall.ENOSYS,
|
||||||
|
"bind /host/nix/store on /sysroot/nix/store: function not implemented"},
|
||||||
|
|
||||||
|
{"remount", &MountError{
|
||||||
|
Source: SourceNone,
|
||||||
|
Target: "/sysroot/nix/store",
|
||||||
|
Fstype: FstypeNULL,
|
||||||
|
Flags: syscall.MS_SILENT | syscall.MS_BIND | syscall.MS_REMOUNT,
|
||||||
|
Data: zeroString,
|
||||||
|
Errno: syscall.EPERM,
|
||||||
|
}, syscall.EPERM,
|
||||||
|
"remount /sysroot/nix/store: operation not permitted"},
|
||||||
|
|
||||||
|
{"overlay", &MountError{
|
||||||
|
Source: SourceOverlay,
|
||||||
|
Target: sysrootPath,
|
||||||
|
Fstype: FstypeOverlay,
|
||||||
|
Data: `lowerdir=/host/var/lib/planterette/base/debian\:f92c9052`,
|
||||||
|
Errno: syscall.EINVAL,
|
||||||
|
}, syscall.EINVAL,
|
||||||
|
"mount overlay on /sysroot: invalid argument"},
|
||||||
|
|
||||||
|
{"fallback", &MountError{
|
||||||
|
Source: SourceNone,
|
||||||
|
Target: sysrootPath,
|
||||||
|
Fstype: FstypeNULL,
|
||||||
|
Errno: syscall.ENOTRECOVERABLE,
|
||||||
|
}, syscall.ENOTRECOVERABLE,
|
||||||
|
"mount /sysroot: state not recoverable"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Run("is", func(t *testing.T) {
|
||||||
|
if !errors.Is(tc.err, tc.errno) {
|
||||||
|
t.Errorf("Is: %#v is not %v", tc.err, tc.errno)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("error", func(t *testing.T) {
|
||||||
|
if got := tc.err.Error(); got != tc.want {
|
||||||
|
t.Errorf("Error: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("zero", func(t *testing.T) {
|
||||||
|
if errors.Is(new(MountError), syscall.Errno(0)) {
|
||||||
|
t.Errorf("Is: zero MountError unexpected true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrnoFallback(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
wantErrno syscall.Errno
|
||||||
|
wantPath *os.PathError
|
||||||
|
}{
|
||||||
|
{"mount", &MountError{
|
||||||
|
Errno: syscall.ENOTRECOVERABLE,
|
||||||
|
}, syscall.ENOTRECOVERABLE, nil},
|
||||||
|
|
||||||
|
{"path errno", &os.PathError{
|
||||||
|
Err: syscall.ETIMEDOUT,
|
||||||
|
}, syscall.ETIMEDOUT, nil},
|
||||||
|
|
||||||
|
{"fallback", stub.UniqueError(0xcafebabe), 0, &os.PathError{
|
||||||
|
Op: "fallback",
|
||||||
|
Path: "/proc/nonexistent",
|
||||||
|
Err: stub.UniqueError(0xcafebabe),
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
errno, err := errnoFallback(tc.name, Nonexistent, tc.err)
|
||||||
|
if errno != tc.wantErrno {
|
||||||
|
t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(err, tc.wantPath) {
|
||||||
|
t.Errorf("errnoFallback: pathError = %#v, want %#v", err, tc.wantPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalMessageFromError exports messageFromError for other tests.
|
||||||
|
func InternalMessageFromError(err error) (string, bool) { return messageFromError(err) }
|
||||||
@ -47,7 +47,9 @@ type (
|
|||||||
// apply is called in intermediate root.
|
// apply is called in intermediate root.
|
||||||
apply(state *setupState, k syscallDispatcher) error
|
apply(state *setupState, k syscallDispatcher) error
|
||||||
|
|
||||||
prefix() 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
|
Is(op Op) bool
|
||||||
Valid() bool
|
Valid() bool
|
||||||
fmt.Stringer
|
fmt.Stringer
|
||||||
@ -68,6 +70,16 @@ const (
|
|||||||
nrAutoRoot
|
nrAutoRoot
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// OpRepeatError is returned applying a repeated nonrepeatable [Op].
|
||||||
|
type OpRepeatError string
|
||||||
|
|
||||||
|
func (e OpRepeatError) Error() string { return string(e) + " is not repeatable" }
|
||||||
|
|
||||||
|
// OpStateError indicates an impossible internal state has been reached in an [Op].
|
||||||
|
type OpStateError string
|
||||||
|
|
||||||
|
func (o OpStateError) Error() string { return "impossible " + string(o) + " state reached" }
|
||||||
|
|
||||||
// initParams are params passed from parent.
|
// initParams are params passed from parent.
|
||||||
type initParams struct {
|
type initParams struct {
|
||||||
Params
|
Params
|
||||||
@ -106,7 +118,7 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
|
|||||||
if errors.Is(err, EBADF) {
|
if errors.Is(err, EBADF) {
|
||||||
k.fatal("invalid setup descriptor")
|
k.fatal("invalid setup descriptor")
|
||||||
}
|
}
|
||||||
if errors.Is(err, ErrNotSet) {
|
if errors.Is(err, ErrReceiveEnv) {
|
||||||
k.fatal("HAKUREI_SETUP not set")
|
k.fatal("HAKUREI_SETUP not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,10 +186,11 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := op.early(state, k); err != nil {
|
if err := op.early(state, k); err != nil {
|
||||||
k.printBaseErr(err,
|
if m, ok := messageFromError(err); ok {
|
||||||
fmt.Sprintf("cannot prepare op at index %d:", i))
|
k.fatal(m)
|
||||||
k.beforeExit()
|
} else {
|
||||||
k.exit(1)
|
k.fatalf("cannot prepare op at index %d: %v", i, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,12 +225,15 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
|
|||||||
chdir is allowed but discouraged */
|
chdir is allowed but discouraged */
|
||||||
for i, op := range *params.Ops {
|
for i, op := range *params.Ops {
|
||||||
// ops already checked during early setup
|
// ops already checked during early setup
|
||||||
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 err := op.apply(state, k); err != nil {
|
||||||
k.printBaseErr(err,
|
if m, ok := messageFromError(err); ok {
|
||||||
fmt.Sprintf("cannot apply op at index %d:", i))
|
k.fatal(m)
|
||||||
k.beforeExit()
|
} else {
|
||||||
k.exit(1)
|
k.fatalf("cannot apply op at index %d: %v", i, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -43,7 +43,7 @@ func (b *BindMountOp) Valid() bool {
|
|||||||
func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
|
func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
|
||||||
if b.Flags&BindEnsure != 0 {
|
if b.Flags&BindEnsure != 0 {
|
||||||
if err := k.mkdirAll(b.Source.String(), 0700); err != nil {
|
if err := k.mkdirAll(b.Source.String(), 0700); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
|
|||||||
// leave sourceFinal as nil
|
// leave sourceFinal as nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else {
|
} else {
|
||||||
b.sourceFinal, err = NewAbs(pathname)
|
b.sourceFinal, err = NewAbs(pathname)
|
||||||
return err
|
return err
|
||||||
@ -63,7 +63,7 @@ func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
|
|||||||
if b.sourceFinal == nil {
|
if b.sourceFinal == nil {
|
||||||
if b.Flags&BindOptional == 0 {
|
if b.Flags&BindOptional == 0 {
|
||||||
// unreachable
|
// unreachable
|
||||||
return msg.WrapErr(os.ErrClosed, "impossible bind state reached")
|
return OpStateError("bind")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -74,10 +74,10 @@ func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
|
|||||||
// this perm value emulates bwrap behaviour as it clears bits from 0755 based on
|
// this perm value emulates bwrap behaviour as it clears bits from 0755 based on
|
||||||
// op->perms which is never set for any bind setup op so always results in 0700
|
// op->perms which is never set for any bind setup op so always results in 0700
|
||||||
if fi, err := k.stat(source); err != nil {
|
if fi, err := k.stat(source); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else if fi.IsDir() {
|
} else if fi.IsDir() {
|
||||||
if err = k.mkdirAll(target, 0700); err != nil {
|
if err = k.mkdirAll(target, 0700); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
} else if err = k.ensureFile(target, 0444, 0700); err != nil {
|
} else if err = k.ensureFile(target, 0444, 0700); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -91,7 +91,12 @@ func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
|
|||||||
flags |= syscall.MS_NODEV
|
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 {
|
func (b *BindMountOp) Is(op Op) bool {
|
||||||
@ -101,7 +106,7 @@ func (b *BindMountOp) Is(op Op) bool {
|
|||||||
b.Target.Is(vb.Target) &&
|
b.Target.Is(vb.Target) &&
|
||||||
b.Flags == vb.Flags
|
b.Flags == vb.Flags
|
||||||
}
|
}
|
||||||
func (*BindMountOp) prefix() string { return "mounting" }
|
func (*BindMountOp) prefix() (string, bool) { return "mounting", false }
|
||||||
func (b *BindMountOp) String() string {
|
func (b *BindMountOp) String() string {
|
||||||
if b.Source == nil || b.Target == nil {
|
if b.Source == nil || b.Target == nil {
|
||||||
return "<invalid>"
|
return "<invalid>"
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBindMountOp(t *testing.T) {
|
func TestBindMountOp(t *testing.T) {
|
||||||
@ -12,138 +14,156 @@ func TestBindMountOp(t *testing.T) {
|
|||||||
{"ENOENT not optional", new(Params), &BindMountOp{
|
{"ENOENT not optional", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT},
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
|
||||||
}, wrapErrSelf(syscall.ENOENT), nil, nil},
|
}, syscall.ENOENT, nil, nil},
|
||||||
|
|
||||||
{"skip optional", new(Params), &BindMountOp{
|
{"skip optional", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
Flags: BindOptional,
|
Flags: BindOptional,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT},
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
|
||||||
}, nil, nil, nil},
|
}, nil, nil, nil},
|
||||||
|
|
||||||
{"success optional", new(Params), &BindMountOp{
|
{"success optional", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
Flags: BindOptional,
|
Flags: BindOptional,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
||||||
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, 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},
|
}, nil},
|
||||||
|
|
||||||
{"ensureFile device", new(Params), &BindMountOp{
|
{"ensureFile device", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/dev/null"),
|
Source: MustAbs("/dev/null"),
|
||||||
Target: MustAbs("/dev/null"),
|
Target: MustAbs("/dev/null"),
|
||||||
Flags: BindWritable | BindDevice,
|
Flags: BindWritable | BindDevice,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
|
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
|
||||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique},
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, stub.UniqueError(5)),
|
||||||
}, errUnique},
|
}, stub.UniqueError(5)},
|
||||||
|
|
||||||
{"mkdirAll ensure", new(Params), &BindMountOp{
|
{"mkdirAll ensure", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
Flags: BindEnsure,
|
Flags: BindEnsure,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/bin/", os.FileMode(0700)}, nil, errUnique},
|
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, stub.UniqueError(4)),
|
||||||
}, wrapErrSelf(errUnique), nil, nil},
|
}, stub.UniqueError(4), nil, nil},
|
||||||
|
|
||||||
{"success ensure", new(Params), &BindMountOp{
|
{"success ensure", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: MustAbs("/usr/bin/"),
|
Target: MustAbs("/usr/bin/"),
|
||||||
Flags: BindEnsure,
|
Flags: BindEnsure,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/bin/", os.FileMode(0700)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, nil),
|
||||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
{"mkdirAll", expectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil),
|
||||||
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, 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},
|
}, nil},
|
||||||
|
|
||||||
{"success device ro", new(Params), &BindMountOp{
|
{"success device ro", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/dev/null"),
|
Source: MustAbs("/dev/null"),
|
||||||
Target: MustAbs("/dev/null"),
|
Target: MustAbs("/dev/null"),
|
||||||
Flags: BindDevice,
|
Flags: BindDevice,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
|
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
|
||||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
||||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, 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},
|
}, nil},
|
||||||
|
|
||||||
{"success device", new(Params), &BindMountOp{
|
{"success device", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/dev/null"),
|
Source: MustAbs("/dev/null"),
|
||||||
Target: MustAbs("/dev/null"),
|
Target: MustAbs("/dev/null"),
|
||||||
Flags: BindWritable | BindDevice,
|
Flags: BindWritable | BindDevice,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
|
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
|
||||||
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
||||||
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, 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},
|
}, nil},
|
||||||
|
|
||||||
{"evalSymlinks", new(Params), &BindMountOp{
|
{"evalSymlinks", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", errUnique},
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", stub.UniqueError(3)),
|
||||||
}, wrapErrSelf(errUnique), nil, nil},
|
}, stub.UniqueError(3), nil, nil},
|
||||||
|
|
||||||
{"stat", new(Params), &BindMountOp{
|
{"stat", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), errUnique},
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), stub.UniqueError(2)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(2)},
|
||||||
|
|
||||||
{"mkdirAll", new(Params), &BindMountOp{
|
{"mkdirAll", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, errUnique},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, stub.UniqueError(1)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(1)},
|
||||||
|
|
||||||
{"bindMount", new(Params), &BindMountOp{
|
{"bindMount", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
||||||
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, errUnique},
|
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
|
||||||
}, errUnique},
|
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{
|
{"success", new(Params), &BindMountOp{
|
||||||
Source: MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
||||||
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, 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},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unreachable", func(t *testing.T) {
|
t.Run("unreachable", func(t *testing.T) {
|
||||||
t.Run("nil sourceFinal not optional", func(t *testing.T) {
|
t.Run("nil sourceFinal not optional", func(t *testing.T) {
|
||||||
wantErr := msg.WrapErr(os.ErrClosed, "impossible bind state reached")
|
wantErr := OpStateError("bind")
|
||||||
if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
|
if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
|
||||||
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,6 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
toHost(FHSDev+name),
|
toHost(FHSDev+name),
|
||||||
targetPath,
|
targetPath,
|
||||||
0,
|
0,
|
||||||
true,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -59,7 +58,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
FHSProc+"self/fd/"+string(rune(i+'0')),
|
FHSProc+"self/fd/"+string(rune(i+'0')),
|
||||||
path.Join(target, name),
|
path.Join(target, name),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, pair := range [][2]string{
|
for _, pair := range [][2]string{
|
||||||
@ -68,7 +67,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
{"pts/ptmx", "ptmx"},
|
{"pts/ptmx", "ptmx"},
|
||||||
} {
|
} {
|
||||||
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
|
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,14 +75,13 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
devPtsPath := path.Join(target, "pts")
|
devPtsPath := path.Join(target, "pts")
|
||||||
for _, name := range []string{devShmPath, devPtsPath} {
|
for _, name := range []string{devShmPath, devPtsPath} {
|
||||||
if err := k.mkdir(name, state.ParentPerm); err != nil {
|
if err := k.mkdir(name, state.ParentPerm); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := k.mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC,
|
if err := k.mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC,
|
||||||
"newinstance,ptmxmode=0666,mode=620"); err != nil {
|
"newinstance,ptmxmode=0666,mode=620"); err != nil {
|
||||||
return wrapErrSuffix(err,
|
return err
|
||||||
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.RetainSession {
|
if state.RetainSession {
|
||||||
@ -93,12 +91,11 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if name, err := k.readlink(hostProc.stdout()); err != nil {
|
if name, err := k.readlink(hostProc.stdout()); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else if err = k.bindMount(
|
} else if err = k.bindMount(
|
||||||
toHost(name),
|
toHost(name),
|
||||||
consolePath,
|
consolePath,
|
||||||
0,
|
0,
|
||||||
false,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -108,10 +105,10 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
if d.Mqueue {
|
if d.Mqueue {
|
||||||
mqueueTarget := path.Join(target, "mqueue")
|
mqueueTarget := path.Join(target, "mqueue")
|
||||||
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
|
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
if err := k.mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil {
|
if err := k.mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil {
|
||||||
return wrapErrSuffix(err, "cannot mount mqueue:")
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,8 +117,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := k.remount(target, MS_RDONLY); err != nil {
|
if err := k.remount(target, MS_RDONLY); err != nil {
|
||||||
return wrapErrSuffix(k.remount(target, MS_RDONLY),
|
return err
|
||||||
fmt.Sprintf("cannot remount %q:", target))
|
|
||||||
}
|
}
|
||||||
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
|
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
|
||||||
}
|
}
|
||||||
@ -133,7 +129,7 @@ func (d *MountDevOp) Is(op Op) bool {
|
|||||||
d.Mqueue == vd.Mqueue &&
|
d.Mqueue == vd.Mqueue &&
|
||||||
d.Write == vd.Write
|
d.Write == vd.Write
|
||||||
}
|
}
|
||||||
func (*MountDevOp) prefix() string { return "mounting" }
|
func (*MountDevOp) prefix() (string, bool) { return "mounting", true }
|
||||||
func (d *MountDevOp) String() string {
|
func (d *MountDevOp) String() string {
|
||||||
if d.Mqueue {
|
if d.Mqueue {
|
||||||
return fmt.Sprintf("dev on %q with mqueue", d.Target)
|
return fmt.Sprintf("dev on %q with mqueue", d.Target)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -23,7 +23,7 @@ type MkdirOp struct {
|
|||||||
func (m *MkdirOp) Valid() bool { return m != nil && m.Path != nil }
|
func (m *MkdirOp) Valid() bool { return m != nil && m.Path != nil }
|
||||||
func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil }
|
func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||||
func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error {
|
func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||||
return wrapErrSelf(k.mkdirAll(toSysroot(m.Path.String()), m.Perm))
|
return k.mkdirAll(toSysroot(m.Path.String()), m.Perm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MkdirOp) Is(op Op) bool {
|
func (m *MkdirOp) Is(op Op) bool {
|
||||||
@ -32,5 +32,5 @@ func (m *MkdirOp) Is(op Op) bool {
|
|||||||
m.Path.Is(vm.Path) &&
|
m.Path.Is(vm.Path) &&
|
||||||
m.Perm == vm.Perm
|
m.Perm == vm.Perm
|
||||||
}
|
}
|
||||||
func (*MkdirOp) prefix() string { return "creating" }
|
func (*MkdirOp) prefix() (string, bool) { return "creating", true }
|
||||||
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
|
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package container
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMkdirOp(t *testing.T) {
|
func TestMkdirOp(t *testing.T) {
|
||||||
@ -10,8 +12,8 @@ func TestMkdirOp(t *testing.T) {
|
|||||||
{"success", new(Params), &MkdirOp{
|
{"success", new(Params), &MkdirOp{
|
||||||
Path: MustAbs("/.hakurei"),
|
Path: MustAbs("/.hakurei"),
|
||||||
Perm: 0500,
|
Perm: 0500,
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/.hakurei", os.FileMode(0500)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/.hakurei", os.FileMode(0500)}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package container
|
|||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -19,6 +18,39 @@ const (
|
|||||||
|
|
||||||
func init() { gob.Register(new(MountOverlayOp)) }
|
func init() { gob.Register(new(MountOverlayOp)) }
|
||||||
|
|
||||||
|
const (
|
||||||
|
// OverlayEphemeralUnexpectedUpper is set when [MountOverlayOp.Work] is nil
|
||||||
|
// and [MountOverlayOp.Upper] holds an unexpected value.
|
||||||
|
OverlayEphemeralUnexpectedUpper = iota
|
||||||
|
// OverlayReadonlyLower is set when [MountOverlayOp.Lower] contains less than
|
||||||
|
// two entries when mounting readonly.
|
||||||
|
OverlayReadonlyLower
|
||||||
|
// OverlayEmptyLower is set when [MountOverlayOp.Lower] has length of zero.
|
||||||
|
OverlayEmptyLower
|
||||||
|
)
|
||||||
|
|
||||||
|
// OverlayArgumentError is returned for [MountOverlayOp] supplied with invalid argument.
|
||||||
|
type OverlayArgumentError struct {
|
||||||
|
Type uintptr
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *OverlayArgumentError) Error() string {
|
||||||
|
switch e.Type {
|
||||||
|
case OverlayEphemeralUnexpectedUpper:
|
||||||
|
return fmt.Sprintf("upperdir has unexpected value %q", e.Value)
|
||||||
|
|
||||||
|
case OverlayReadonlyLower:
|
||||||
|
return "readonly overlay requires at least two lowerdir"
|
||||||
|
|
||||||
|
case OverlayEmptyLower:
|
||||||
|
return "overlay requires at least one lowerdir"
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("invalid overlay argument error %#x", e.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target].
|
// Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target].
|
||||||
func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops {
|
func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops {
|
||||||
*f = append(*f, &MountOverlayOp{
|
*f = append(*f, &MountOverlayOp{
|
||||||
@ -89,7 +121,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
|
|||||||
o.ephemeral = true // intermediate root not yet available
|
o.ephemeral = true // intermediate root not yet available
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("upperdir has unexpected value %q", o.Upper))
|
return &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, o.Upper.String()}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// readonly handled in apply
|
// readonly handled in apply
|
||||||
@ -97,12 +129,12 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
|
|||||||
if !o.ephemeral {
|
if !o.ephemeral {
|
||||||
if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) {
|
if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) {
|
||||||
// unreachable
|
// unreachable
|
||||||
return msg.WrapErr(fs.ErrClosed, "impossible overlay state reached")
|
return OpStateError("overlay")
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.Upper != nil {
|
if o.Upper != nil {
|
||||||
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
|
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else {
|
} else {
|
||||||
o.upper = EscapeOverlayDataSegment(toHost(v))
|
o.upper = EscapeOverlayDataSegment(toHost(v))
|
||||||
}
|
}
|
||||||
@ -110,7 +142,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
|
|||||||
|
|
||||||
if o.Work != nil {
|
if o.Work != nil {
|
||||||
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
|
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else {
|
} else {
|
||||||
o.work = EscapeOverlayDataSegment(toHost(v))
|
o.work = EscapeOverlayDataSegment(toHost(v))
|
||||||
}
|
}
|
||||||
@ -120,7 +152,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
|
|||||||
o.lower = make([]string, len(o.Lower))
|
o.lower = make([]string, len(o.Lower))
|
||||||
for i, a := range o.Lower { // nil checked in Valid
|
for i, a := range o.Lower { // nil checked in Valid
|
||||||
if v, err := k.evalSymlinks(a.String()); err != nil {
|
if v, err := k.evalSymlinks(a.String()); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else {
|
} else {
|
||||||
o.lower[i] = EscapeOverlayDataSegment(toHost(v))
|
o.lower[i] = EscapeOverlayDataSegment(toHost(v))
|
||||||
}
|
}
|
||||||
@ -134,17 +166,17 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
target = toSysroot(target)
|
target = toSysroot(target)
|
||||||
}
|
}
|
||||||
if err := k.mkdirAll(target, state.ParentPerm); err != nil {
|
if err := k.mkdirAll(target, state.ParentPerm); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.ephemeral {
|
if o.ephemeral {
|
||||||
var err error
|
var err error
|
||||||
// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
|
// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
|
||||||
if o.upper, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil {
|
if o.upper, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
if o.work, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil {
|
if o.work, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,12 +184,12 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
|
|
||||||
if o.upper == zeroString && o.work == zeroString { // readonly
|
if o.upper == zeroString && o.work == zeroString { // readonly
|
||||||
if len(o.Lower) < 2 {
|
if len(o.Lower) < 2 {
|
||||||
return msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir")
|
return &OverlayArgumentError{OverlayReadonlyLower, zeroString}
|
||||||
}
|
}
|
||||||
// "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only
|
// "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only
|
||||||
} else {
|
} else {
|
||||||
if len(o.Lower) == 0 {
|
if len(o.Lower) == 0 {
|
||||||
return msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir")
|
return &OverlayArgumentError{OverlayEmptyLower, zeroString}
|
||||||
}
|
}
|
||||||
options = append(options,
|
options = append(options,
|
||||||
OptionOverlayUpperdir+"="+o.upper,
|
OptionOverlayUpperdir+"="+o.upper,
|
||||||
@ -167,8 +199,7 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath),
|
OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath),
|
||||||
OptionOverlayUserxattr)
|
OptionOverlayUserxattr)
|
||||||
|
|
||||||
return wrapErrSuffix(k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)),
|
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption))
|
||||||
fmt.Sprintf("cannot mount overlay on %q:", o.Target))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *MountOverlayOp) Is(op Op) bool {
|
func (o *MountOverlayOp) Is(op Op) bool {
|
||||||
@ -178,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) }) &&
|
slices.EqualFunc(o.Lower, vo.Lower, func(a *Absolute, v *Absolute) bool { return a.Is(v) }) &&
|
||||||
o.Upper.Is(vo.Upper) && o.Work.Is(vo.Work)
|
o.Upper.Is(vo.Upper) && o.Work.Is(vo.Work)
|
||||||
}
|
}
|
||||||
func (*MountOverlayOp) prefix() string { return "mounting" }
|
func (*MountOverlayOp) prefix() (string, bool) { return "mounting", true }
|
||||||
func (o *MountOverlayOp) String() string {
|
func (o *MountOverlayOp) String() string {
|
||||||
return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower))
|
return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,40 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMountOverlayOp(t *testing.T) {
|
func TestMountOverlayOp(t *testing.T) {
|
||||||
|
t.Run("argument error", func(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err *OverlayArgumentError
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"unexpected upper", &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, "/proc/"},
|
||||||
|
`upperdir has unexpected value "/proc/"`},
|
||||||
|
|
||||||
|
{"lower ro short", &OverlayArgumentError{OverlayReadonlyLower, zeroString},
|
||||||
|
"readonly overlay requires at least two lowerdir"},
|
||||||
|
|
||||||
|
{"lower short", &OverlayArgumentError{OverlayEmptyLower, zeroString},
|
||||||
|
"overlay requires at least one lowerdir"},
|
||||||
|
|
||||||
|
{"oob", &OverlayArgumentError{0xdeadbeef, zeroString},
|
||||||
|
"invalid overlay argument error 0xdeadbeef"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := tc.err.Error(); got != tc.want {
|
||||||
|
t.Errorf("Error: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||||
{"mkdirTemp invalid ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
|
{"mkdirTemp invalid ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
|
||||||
Target: MustAbs("/"),
|
Target: MustAbs("/"),
|
||||||
@ -16,7 +44,7 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
|
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
|
||||||
},
|
},
|
||||||
Upper: MustAbs("/proc/"),
|
Upper: MustAbs("/proc/"),
|
||||||
}, nil, msg.WrapErr(fs.ErrInvalid, `upperdir has unexpected value "/proc/"`), nil, nil},
|
}, nil, &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, "/proc/"}, nil, nil},
|
||||||
|
|
||||||
{"mkdirTemp upper ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
|
{"mkdirTemp upper ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
|
||||||
Target: MustAbs("/"),
|
Target: MustAbs("/"),
|
||||||
@ -25,13 +53,13 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
|
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
|
||||||
},
|
},
|
||||||
Upper: MustAbs("/"),
|
Upper: MustAbs("/"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
|
||||||
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", errUnique},
|
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", stub.UniqueError(6)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(6)},
|
||||||
|
|
||||||
{"mkdirTemp work ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
|
{"mkdirTemp work ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
|
||||||
Target: MustAbs("/"),
|
Target: MustAbs("/"),
|
||||||
@ -40,14 +68,14 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
|
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
|
||||||
},
|
},
|
||||||
Upper: MustAbs("/"),
|
Upper: MustAbs("/"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
|
||||||
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil},
|
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
|
||||||
{"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", errUnique},
|
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", stub.UniqueError(5)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(5)},
|
||||||
|
|
||||||
{"success ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
|
{"success ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
|
||||||
Target: MustAbs("/"),
|
Target: MustAbs("/"),
|
||||||
@ -56,20 +84,20 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
|
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
|
||||||
},
|
},
|
||||||
Upper: MustAbs("/"),
|
Upper: MustAbs("/"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
|
||||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
|
||||||
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil},
|
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
|
||||||
{"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil},
|
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil),
|
||||||
{"mount", expectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" +
|
call("mount", stub.ExpectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" +
|
||||||
"upperdir=overlay.upper.32768," +
|
"upperdir=overlay.upper.32768," +
|
||||||
"workdir=overlay.work.32768," +
|
"workdir=overlay.work.32768," +
|
||||||
"lowerdir=" +
|
"lowerdir=" +
|
||||||
`/host/var/lib/planterette/base/debian\:f92c9052:` +
|
`/host/var/lib/planterette/base/debian\:f92c9052:` +
|
||||||
`/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` +
|
`/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` +
|
||||||
"userxattr"}, nil, nil},
|
"userxattr"}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
||||||
@ -77,11 +105,11 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
Lower: []*Absolute{
|
Lower: []*Absolute{
|
||||||
MustAbs("/mnt-root/nix/.ro-store"),
|
MustAbs("/mnt-root/nix/.ro-store"),
|
||||||
},
|
},
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
|
||||||
}, msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir")},
|
}, &OverlayArgumentError{OverlayReadonlyLower, zeroString}},
|
||||||
|
|
||||||
{"success ro noPrefix", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
{"success ro noPrefix", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
||||||
Target: MustAbs("/nix/store"),
|
Target: MustAbs("/nix/store"),
|
||||||
@ -90,16 +118,16 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
MustAbs("/mnt-root/nix/.ro-store0"),
|
MustAbs("/mnt-root/nix/.ro-store0"),
|
||||||
},
|
},
|
||||||
noPrefix: true,
|
noPrefix: true,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/nix/store", os.FileMode(0755)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/nix/store", os.FileMode(0755)}, nil, nil),
|
||||||
{"mount", expectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" +
|
call("mount", stub.ExpectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" +
|
||||||
"lowerdir=" +
|
"lowerdir=" +
|
||||||
"/host/mnt-root/nix/.ro-store:" +
|
"/host/mnt-root/nix/.ro-store:" +
|
||||||
"/host/mnt-root/nix/.ro-store0," +
|
"/host/mnt-root/nix/.ro-store0," +
|
||||||
"userxattr"}, nil, nil},
|
"userxattr"}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
||||||
@ -108,102 +136,102 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
MustAbs("/mnt-root/nix/.ro-store"),
|
MustAbs("/mnt-root/nix/.ro-store"),
|
||||||
MustAbs("/mnt-root/nix/.ro-store0"),
|
MustAbs("/mnt-root/nix/.ro-store0"),
|
||||||
},
|
},
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
|
||||||
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
||||||
"lowerdir=" +
|
"lowerdir=" +
|
||||||
"/host/mnt-root/nix/.ro-store:" +
|
"/host/mnt-root/nix/.ro-store:" +
|
||||||
"/host/mnt-root/nix/.ro-store0," +
|
"/host/mnt-root/nix/.ro-store0," +
|
||||||
"userxattr"}, nil, nil},
|
"userxattr"}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||||
Target: MustAbs("/nix/store"),
|
Target: MustAbs("/nix/store"),
|
||||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||||
}, msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir")},
|
}, &OverlayArgumentError{OverlayEmptyLower, zeroString}},
|
||||||
|
|
||||||
{"evalSymlinks upper", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
{"evalSymlinks upper", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||||
Target: MustAbs("/nix/store"),
|
Target: MustAbs("/nix/store"),
|
||||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", errUnique},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", stub.UniqueError(4)),
|
||||||
}, wrapErrSelf(errUnique), nil, nil},
|
}, stub.UniqueError(4), nil, nil},
|
||||||
|
|
||||||
{"evalSymlinks work", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
{"evalSymlinks work", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||||
Target: MustAbs("/nix/store"),
|
Target: MustAbs("/nix/store"),
|
||||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", errUnique},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", stub.UniqueError(3)),
|
||||||
}, wrapErrSelf(errUnique), nil, nil},
|
}, stub.UniqueError(3), nil, nil},
|
||||||
|
|
||||||
{"evalSymlinks lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
{"evalSymlinks lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||||
Target: MustAbs("/nix/store"),
|
Target: MustAbs("/nix/store"),
|
||||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", errUnique},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", stub.UniqueError(2)),
|
||||||
}, wrapErrSelf(errUnique), nil, nil},
|
}, stub.UniqueError(2), nil, nil},
|
||||||
|
|
||||||
{"mkdirAll", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
{"mkdirAll", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||||
Target: MustAbs("/nix/store"),
|
Target: MustAbs("/nix/store"),
|
||||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, errUnique},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, stub.UniqueError(1)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(1)},
|
||||||
|
|
||||||
{"mount", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
{"mount", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||||
Target: MustAbs("/nix/store"),
|
Target: MustAbs("/nix/store"),
|
||||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||||
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, errUnique},
|
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, stub.UniqueError(0)),
|
||||||
}, wrapErrSuffix(errUnique, `cannot mount overlay on "/nix/store":`)},
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||||
Target: MustAbs("/nix/store"),
|
Target: MustAbs("/nix/store"),
|
||||||
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
|
||||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||||
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
||||||
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
|
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
|
||||||
"workdir=/host/mnt-root/nix/.rw-store/.work," +
|
"workdir=/host/mnt-root/nix/.rw-store/.work," +
|
||||||
"lowerdir=/host/mnt-root/nix/ro-store," +
|
"lowerdir=/host/mnt-root/nix/ro-store," +
|
||||||
"userxattr"}, nil, nil},
|
"userxattr"}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||||
@ -217,17 +245,17 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
|
||||||
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/ro-store0", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/ro-store0", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store1"}, "/mnt-root/nix/ro-store1", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store1"}, "/mnt-root/nix/ro-store1", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store2"}, "/mnt-root/nix/ro-store2", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store2"}, "/mnt-root/nix/ro-store2", nil),
|
||||||
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||||
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
||||||
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
|
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
|
||||||
"workdir=/host/mnt-root/nix/.rw-store/.work," +
|
"workdir=/host/mnt-root/nix/.rw-store/.work," +
|
||||||
"lowerdir=" +
|
"lowerdir=" +
|
||||||
@ -236,13 +264,13 @@ func TestMountOverlayOp(t *testing.T) {
|
|||||||
"/host/mnt-root/nix/ro-store1:" +
|
"/host/mnt-root/nix/ro-store1:" +
|
||||||
"/host/mnt-root/nix/ro-store2:" +
|
"/host/mnt-root/nix/ro-store2:" +
|
||||||
"/host/mnt-root/nix/ro-store3," +
|
"/host/mnt-root/nix/ro-store3," +
|
||||||
"userxattr"}, nil, nil},
|
"userxattr"}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unreachable", func(t *testing.T) {
|
t.Run("unreachable", func(t *testing.T) {
|
||||||
t.Run("nil Upper non-nil Work not ephemeral", func(t *testing.T) {
|
t.Run("nil Upper non-nil Work not ephemeral", func(t *testing.T) {
|
||||||
wantErr := msg.WrapErr(fs.ErrClosed, "impossible overlay state reached")
|
wantErr := OpStateError("overlay")
|
||||||
if err := (&MountOverlayOp{
|
if err := (&MountOverlayOp{
|
||||||
Work: MustAbs("/"),
|
Work: MustAbs("/"),
|
||||||
}).early(nil, nil); !errors.Is(err, wantErr) {
|
}).early(nil, nil); !errors.Is(err, wantErr) {
|
||||||
|
|||||||
@ -39,13 +39,11 @@ func (t *TmpfileOp) early(*setupState, syscallDispatcher) error { return nil }
|
|||||||
func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
|
func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
|
||||||
var tmpPath string
|
var tmpPath string
|
||||||
if f, err := k.createTemp(FHSRoot, intermediatePatternTmpfile); err != nil {
|
if f, err := k.createTemp(FHSRoot, intermediatePatternTmpfile); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else if _, err = f.Write(t.Data); err != nil {
|
} else if _, err = f.Write(t.Data); err != nil {
|
||||||
return wrapErrSuffix(err,
|
return err
|
||||||
"cannot write to intermediate file:")
|
|
||||||
} else if err = f.Close(); err != nil {
|
} else if err = f.Close(); err != nil {
|
||||||
return wrapErrSuffix(err,
|
return err
|
||||||
"cannot close intermediate file:")
|
|
||||||
} else {
|
} else {
|
||||||
tmpPath = f.Name()
|
tmpPath = f.Name()
|
||||||
}
|
}
|
||||||
@ -57,11 +55,10 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
tmpPath,
|
tmpPath,
|
||||||
target,
|
target,
|
||||||
syscall.MS_RDONLY|syscall.MS_NODEV,
|
syscall.MS_RDONLY|syscall.MS_NODEV,
|
||||||
false,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if err = k.remove(tmpPath); err != nil {
|
} else if err = k.remove(tmpPath); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -72,7 +69,7 @@ func (t *TmpfileOp) Is(op Op) bool {
|
|||||||
t.Path.Is(vt.Path) &&
|
t.Path.Is(vt.Path) &&
|
||||||
string(t.Data) == string(vt.Data)
|
string(t.Data) == string(vt.Data)
|
||||||
}
|
}
|
||||||
func (*TmpfileOp) prefix() string { return "placing" }
|
func (*TmpfileOp) prefix() (string, bool) { return "placing", true }
|
||||||
func (t *TmpfileOp) String() string {
|
func (t *TmpfileOp) String() string {
|
||||||
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
|
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package container
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTmpfileOp(t *testing.T) {
|
func TestTmpfileOp(t *testing.T) {
|
||||||
@ -16,59 +18,59 @@ func TestTmpfileOp(t *testing.T) {
|
|||||||
{"createTemp", &Params{ParentPerm: 0700}, &TmpfileOp{
|
{"createTemp", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
Path: samplePath,
|
Path: samplePath,
|
||||||
Data: sampleData,
|
Data: sampleData,
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), errUnique},
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), stub.UniqueError(5)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(5)},
|
||||||
|
|
||||||
{"Write", &Params{ParentPerm: 0700}, &TmpfileOp{
|
{"Write", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
Path: samplePath,
|
Path: samplePath,
|
||||||
Data: sampleData,
|
Data: sampleData,
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"createTemp", expectArgs{"/", "tmp.*"}, writeErrOsFile{errUnique}, nil},
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, writeErrOsFile{stub.UniqueError(4)}, nil),
|
||||||
}, wrapErrSuffix(errUnique, "cannot write to intermediate file:")},
|
}, stub.UniqueError(4)},
|
||||||
|
|
||||||
{"Close", &Params{ParentPerm: 0700}, &TmpfileOp{
|
{"Close", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
Path: samplePath,
|
Path: samplePath,
|
||||||
Data: sampleData,
|
Data: sampleData,
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, errUnique), nil},
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, stub.UniqueError(3)), nil),
|
||||||
}, wrapErrSuffix(errUnique, "cannot close intermediate file:")},
|
}, stub.UniqueError(3)},
|
||||||
|
|
||||||
{"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{
|
{"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
Path: samplePath,
|
Path: samplePath,
|
||||||
Data: sampleData,
|
Data: sampleData,
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
|
||||||
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique},
|
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, stub.UniqueError(2)),
|
||||||
}, errUnique},
|
}, stub.UniqueError(2)},
|
||||||
|
|
||||||
{"bindMount", &Params{ParentPerm: 0700}, &TmpfileOp{
|
{"bindMount", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
Path: samplePath,
|
Path: samplePath,
|
||||||
Data: sampleData,
|
Data: sampleData,
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
|
||||||
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
|
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
||||||
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, errUnique},
|
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, stub.UniqueError(1)),
|
||||||
}, errUnique},
|
}, stub.UniqueError(1)},
|
||||||
|
|
||||||
{"remove", &Params{ParentPerm: 0700}, &TmpfileOp{
|
{"remove", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
Path: samplePath,
|
Path: samplePath,
|
||||||
Data: sampleData,
|
Data: sampleData,
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
|
||||||
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
|
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
||||||
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil},
|
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil),
|
||||||
{"remove", expectArgs{"tmp.32768"}, nil, errUnique},
|
call("remove", stub.ExpectArgs{"tmp.32768"}, nil, stub.UniqueError(0)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
{"success", &Params{ParentPerm: 0700}, &TmpfileOp{
|
{"success", &Params{ParentPerm: 0700}, &TmpfileOp{
|
||||||
Path: samplePath,
|
Path: samplePath,
|
||||||
Data: sampleData,
|
Data: sampleData,
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
|
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
|
||||||
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
|
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
||||||
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil},
|
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil),
|
||||||
{"remove", expectArgs{"tmp.32768"}, nil, nil},
|
call("remove", stub.ExpectArgs{"tmp.32768"}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -22,10 +22,9 @@ func (p *MountProcOp) early(*setupState, syscallDispatcher) error { return nil }
|
|||||||
func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error {
|
func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error {
|
||||||
target := toSysroot(p.Target.String())
|
target := toSysroot(p.Target.String())
|
||||||
if err := k.mkdirAll(target, state.ParentPerm); err != nil {
|
if err := k.mkdirAll(target, state.ParentPerm); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
return wrapErrSuffix(k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString),
|
return k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString)
|
||||||
fmt.Sprintf("cannot mount proc on %q:", p.Target.String()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MountProcOp) Is(op Op) bool {
|
func (p *MountProcOp) Is(op Op) bool {
|
||||||
@ -33,5 +32,5 @@ func (p *MountProcOp) Is(op Op) bool {
|
|||||||
return ok && p.Valid() && vp.Valid() &&
|
return ok && p.Valid() && vp.Valid() &&
|
||||||
p.Target.Is(vp.Target)
|
p.Target.Is(vp.Target)
|
||||||
}
|
}
|
||||||
func (*MountProcOp) prefix() string { return "mounting" }
|
func (*MountProcOp) prefix() (string, bool) { return "mounting", true }
|
||||||
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }
|
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package container
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMountProcOp(t *testing.T) {
|
func TestMountProcOp(t *testing.T) {
|
||||||
@ -10,16 +12,16 @@ func TestMountProcOp(t *testing.T) {
|
|||||||
{"mkdir", &Params{ParentPerm: 0755},
|
{"mkdir", &Params{ParentPerm: 0755},
|
||||||
&MountProcOp{
|
&MountProcOp{
|
||||||
Target: MustAbs("/proc/"),
|
Target: MustAbs("/proc/"),
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, errUnique},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, stub.UniqueError(0)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
{"success", &Params{ParentPerm: 0700},
|
{"success", &Params{ParentPerm: 0700},
|
||||||
&MountProcOp{
|
&MountProcOp{
|
||||||
Target: MustAbs("/proc/"),
|
Target: MustAbs("/proc/"),
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil),
|
||||||
{"mount", expectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -22,8 +22,7 @@ type RemountOp struct {
|
|||||||
func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil }
|
func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil }
|
||||||
func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
|
func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||||
func (r *RemountOp) apply(_ *setupState, k syscallDispatcher) error {
|
func (r *RemountOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||||
return wrapErrSuffix(k.remount(toSysroot(r.Target.String()), r.Flags),
|
return k.remount(toSysroot(r.Target.String()), r.Flags)
|
||||||
fmt.Sprintf("cannot remount %q:", r.Target))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RemountOp) Is(op Op) bool {
|
func (r *RemountOp) Is(op Op) bool {
|
||||||
@ -32,5 +31,5 @@ func (r *RemountOp) Is(op Op) bool {
|
|||||||
r.Target.Is(vr.Target) &&
|
r.Target.Is(vr.Target) &&
|
||||||
r.Flags == vr.Flags
|
r.Flags == vr.Flags
|
||||||
}
|
}
|
||||||
func (*RemountOp) prefix() string { return "remounting" }
|
func (*RemountOp) prefix() (string, bool) { return "remounting", true }
|
||||||
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }
|
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package container
|
|||||||
import (
|
import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRemountOp(t *testing.T) {
|
func TestRemountOp(t *testing.T) {
|
||||||
@ -10,8 +12,8 @@ func TestRemountOp(t *testing.T) {
|
|||||||
{"success", new(Params), &RemountOp{
|
{"success", new(Params), &RemountOp{
|
||||||
Target: MustAbs("/"),
|
Target: MustAbs("/"),
|
||||||
Flags: syscall.MS_RDONLY,
|
Flags: syscall.MS_RDONLY,
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"remount", expectArgs{"/sysroot", uintptr(1)}, nil, nil},
|
call("remount", stub.ExpectArgs{"/sysroot", uintptr(1)}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package container
|
|||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"path"
|
"path"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,10 +29,10 @@ func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkN
|
|||||||
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
|
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
|
||||||
if l.Dereference {
|
if l.Dereference {
|
||||||
if !isAbs(l.LinkName) {
|
if !isAbs(l.LinkName) {
|
||||||
return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("path %q is not absolute", l.LinkName))
|
return &AbsoluteError{l.LinkName}
|
||||||
}
|
}
|
||||||
if name, err := k.readlink(l.LinkName); err != nil {
|
if name, err := k.readlink(l.LinkName); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else {
|
} else {
|
||||||
l.LinkName = name
|
l.LinkName = name
|
||||||
}
|
}
|
||||||
@ -44,9 +43,9 @@ func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
|
|||||||
func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
|
func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
|
||||||
target := toSysroot(l.Target.String())
|
target := toSysroot(l.Target.String())
|
||||||
if err := k.mkdirAll(path.Dir(target), state.ParentPerm); err != nil {
|
if err := k.mkdirAll(path.Dir(target), state.ParentPerm); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
return wrapErrSelf(k.symlink(l.LinkName, target))
|
return k.symlink(l.LinkName, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *SymlinkOp) Is(op Op) bool {
|
func (l *SymlinkOp) Is(op Op) bool {
|
||||||
@ -56,7 +55,7 @@ func (l *SymlinkOp) Is(op Op) bool {
|
|||||||
l.LinkName == vl.LinkName &&
|
l.LinkName == vl.LinkName &&
|
||||||
l.Dereference == vl.Dereference
|
l.Dereference == vl.Dereference
|
||||||
}
|
}
|
||||||
func (*SymlinkOp) prefix() string { return "creating" }
|
func (*SymlinkOp) prefix() (string, bool) { return "creating", true }
|
||||||
func (l *SymlinkOp) String() string {
|
func (l *SymlinkOp) String() string {
|
||||||
return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName)
|
return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSymlinkOp(t *testing.T) {
|
func TestSymlinkOp(t *testing.T) {
|
||||||
@ -11,41 +12,41 @@ func TestSymlinkOp(t *testing.T) {
|
|||||||
{"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{
|
{"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{
|
||||||
Target: MustAbs("/etc/nixos"),
|
Target: MustAbs("/etc/nixos"),
|
||||||
LinkName: "/etc/static/nixos",
|
LinkName: "/etc/static/nixos",
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, errUnique},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, stub.UniqueError(1)),
|
||||||
}, wrapErrSelf(errUnique)},
|
}, stub.UniqueError(1)},
|
||||||
|
|
||||||
{"abs", &Params{ParentPerm: 0755}, &SymlinkOp{
|
{"abs", &Params{ParentPerm: 0755}, &SymlinkOp{
|
||||||
Target: MustAbs("/etc/mtab"),
|
Target: MustAbs("/etc/mtab"),
|
||||||
LinkName: "etc/mtab",
|
LinkName: "etc/mtab",
|
||||||
Dereference: true,
|
Dereference: true,
|
||||||
}, nil, msg.WrapErr(fs.ErrInvalid, `path "etc/mtab" is not absolute`), nil, nil},
|
}, nil, &AbsoluteError{"etc/mtab"}, nil, nil},
|
||||||
|
|
||||||
{"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
|
{"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
|
||||||
Target: MustAbs("/etc/mtab"),
|
Target: MustAbs("/etc/mtab"),
|
||||||
LinkName: "/etc/mtab",
|
LinkName: "/etc/mtab",
|
||||||
Dereference: true,
|
Dereference: true,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", errUnique},
|
call("readlink", stub.ExpectArgs{"/etc/mtab"}, "/proc/mounts", stub.UniqueError(0)),
|
||||||
}, wrapErrSelf(errUnique), nil, nil},
|
}, stub.UniqueError(0), nil, nil},
|
||||||
|
|
||||||
{"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{
|
{"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{
|
||||||
Target: MustAbs("/etc/nixos"),
|
Target: MustAbs("/etc/nixos"),
|
||||||
LinkName: "/etc/static/nixos",
|
LinkName: "/etc/static/nixos",
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil),
|
||||||
{"symlink", expectArgs{"/etc/static/nixos", "/sysroot/etc/nixos"}, nil, nil},
|
call("symlink", stub.ExpectArgs{"/etc/static/nixos", "/sysroot/etc/nixos"}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"success", &Params{ParentPerm: 0755}, &SymlinkOp{
|
{"success", &Params{ParentPerm: 0755}, &SymlinkOp{
|
||||||
Target: MustAbs("/etc/mtab"),
|
Target: MustAbs("/etc/mtab"),
|
||||||
LinkName: "/etc/mtab",
|
LinkName: "/etc/mtab",
|
||||||
Dereference: true,
|
Dereference: true,
|
||||||
}, []kexpect{
|
}, []stub.Call{
|
||||||
{"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", nil},
|
call("readlink", stub.ExpectArgs{"/etc/mtab"}, "/proc/mounts", nil),
|
||||||
}, nil, []kexpect{
|
}, nil, []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0755)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0755)}, nil, nil),
|
||||||
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
|
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -3,14 +3,20 @@ package container
|
|||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
. "syscall"
|
. "syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { gob.Register(new(MountTmpfsOp)) }
|
func init() { gob.Register(new(MountTmpfsOp)) }
|
||||||
|
|
||||||
|
type TmpfsSizeError int
|
||||||
|
|
||||||
|
func (e TmpfsSizeError) Error() string {
|
||||||
|
return "tmpfs size " + strconv.Itoa(int(e)) + " out of bounds"
|
||||||
|
}
|
||||||
|
|
||||||
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
|
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
|
||||||
func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops {
|
func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops {
|
||||||
*f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm})
|
*f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm})
|
||||||
@ -36,7 +42,7 @@ func (t *MountTmpfsOp) Valid() bool { return t !=
|
|||||||
func (t *MountTmpfsOp) early(*setupState, syscallDispatcher) error { return nil }
|
func (t *MountTmpfsOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||||
func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error {
|
func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||||
if t.Size < 0 || t.Size > math.MaxUint>>1 {
|
if t.Size < 0 || t.Size > math.MaxUint>>1 {
|
||||||
return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("size %d out of bounds", t.Size))
|
return TmpfsSizeError(t.Size)
|
||||||
}
|
}
|
||||||
return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
|
return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
|
||||||
}
|
}
|
||||||
@ -50,5 +56,5 @@ func (t *MountTmpfsOp) Is(op Op) bool {
|
|||||||
t.Size == vt.Size &&
|
t.Size == vt.Size &&
|
||||||
t.Perm == vt.Perm
|
t.Perm == vt.Perm
|
||||||
}
|
}
|
||||||
func (*MountTmpfsOp) prefix() string { return "mounting" }
|
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) }
|
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
|
||||||
|
|||||||
@ -1,31 +1,40 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMountTmpfsOp(t *testing.T) {
|
func TestMountTmpfsOp(t *testing.T) {
|
||||||
|
t.Run("size error", func(t *testing.T) {
|
||||||
|
tmpfsSizeError := TmpfsSizeError(-1)
|
||||||
|
want := "tmpfs size -1 out of bounds"
|
||||||
|
if got := tmpfsSizeError.Error(); got != want {
|
||||||
|
t.Errorf("Error: %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||||
{"size oob", new(Params), &MountTmpfsOp{
|
{"size oob", new(Params), &MountTmpfsOp{
|
||||||
Size: -1,
|
Size: -1,
|
||||||
}, nil, nil, nil, msg.WrapErr(fs.ErrInvalid, "size -1 out of bounds")},
|
}, nil, nil, nil, TmpfsSizeError(-1)},
|
||||||
|
|
||||||
{"success", new(Params), &MountTmpfsOp{
|
{"success", new(Params), &MountTmpfsOp{
|
||||||
FSName: "ephemeral",
|
FSName: "ephemeral",
|
||||||
Path: MustAbs("/run/user/1000/"),
|
Path: MustAbs("/run/user/1000/"),
|
||||||
Size: 1 << 10,
|
Size: 1 << 10,
|
||||||
Perm: 0700,
|
Perm: 0700,
|
||||||
}, nil, nil, []kexpect{
|
}, nil, nil, []stub.Call{
|
||||||
{"mountTmpfs", expectArgs{
|
call("mountTmpfs", stub.ExpectArgs{
|
||||||
"ephemeral", // fsname
|
"ephemeral", // fsname
|
||||||
"/sysroot/run/user/1000", // target
|
"/sysroot/run/user/1000", // target
|
||||||
uintptr(0), // flags
|
uintptr(0), // flags
|
||||||
0x400, // size
|
0x400, // size
|
||||||
os.FileMode(0700), // perm
|
os.FileMode(0700), // perm
|
||||||
}, nil, nil},
|
}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -96,18 +96,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// bindMount mounts source on target and recursively applies flags if MS_REC is set.
|
// bindMount mounts source on target and recursively applies flags if MS_REC is set.
|
||||||
func (p *procPaths) bindMount(source, target string, flags uintptr, 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
|
// syscallDispatcher.bindMount and procPaths.remount must not be called from this function
|
||||||
|
|
||||||
if eq {
|
|
||||||
p.k.verbosef("resolved %q flags %#x", target, flags)
|
|
||||||
} else {
|
|
||||||
p.k.verbosef("resolved %q on %q flags %#x", source, target, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
|
if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
|
||||||
return wrapErrSuffix(err,
|
return err
|
||||||
fmt.Sprintf("cannot mount %q on %q:", source, target))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.k.remount(target, flags)
|
return p.k.remount(target, flags)
|
||||||
@ -119,7 +112,7 @@ func (p *procPaths) remount(target string, flags uintptr) error {
|
|||||||
|
|
||||||
var targetFinal string
|
var targetFinal string
|
||||||
if v, err := p.k.evalSymlinks(target); err != nil {
|
if v, err := p.k.evalSymlinks(target); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else {
|
} else {
|
||||||
targetFinal = v
|
targetFinal = v
|
||||||
if targetFinal != target {
|
if targetFinal != target {
|
||||||
@ -135,14 +128,12 @@ func (p *procPaths) remount(target string, flags uintptr) error {
|
|||||||
destFd, err = p.k.open(targetFinal, O_PATH|O_CLOEXEC, 0)
|
destFd, err = p.k.open(targetFinal, O_PATH|O_CLOEXEC, 0)
|
||||||
return
|
return
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return wrapErrSuffix(err,
|
return &os.PathError{Op: "open", Path: targetFinal, Err: err}
|
||||||
fmt.Sprintf("cannot open %q:", targetFinal))
|
|
||||||
}
|
}
|
||||||
if v, err := p.k.readlink(p.fd(destFd)); err != nil {
|
if v, err := p.k.readlink(p.fd(destFd)); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else if err = p.k.close(destFd); err != nil {
|
} else if err = p.k.close(destFd); err != nil {
|
||||||
return wrapErrSuffix(err,
|
return &os.PathError{Op: "close", Path: targetFinal, Err: err}
|
||||||
fmt.Sprintf("cannot close %q:", targetFinal))
|
|
||||||
} else {
|
} else {
|
||||||
targetKFinal = v
|
targetKFinal = v
|
||||||
}
|
}
|
||||||
@ -152,17 +143,11 @@ func (p *procPaths) remount(target string, flags uintptr) error {
|
|||||||
return p.mountinfo(func(d *vfs.MountInfoDecoder) error {
|
return p.mountinfo(func(d *vfs.MountInfoDecoder) error {
|
||||||
n, err := d.Unfold(targetKFinal)
|
n, err := d.Unfold(targetKFinal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ESTALE) {
|
return err
|
||||||
return msg.WrapErr(err,
|
|
||||||
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
|
|
||||||
}
|
|
||||||
return wrapErrSuffix(err,
|
|
||||||
"cannot unfold mount hierarchy:")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = remountWithFlags(p.k, n, mf); err != nil {
|
if err = remountWithFlags(p.k, n, mf); err != nil {
|
||||||
return wrapErrSuffix(err,
|
return err
|
||||||
fmt.Sprintf("cannot remount %q:", n.Clean))
|
|
||||||
}
|
}
|
||||||
if flags&MS_REC == 0 {
|
if flags&MS_REC == 0 {
|
||||||
return nil
|
return nil
|
||||||
@ -174,11 +159,8 @@ func (p *procPaths) remount(target string, flags uintptr) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = remountWithFlags(p.k, cur, mf)
|
if err = remountWithFlags(p.k, cur, mf); err != nil && !errors.Is(err, EACCES) {
|
||||||
|
return err
|
||||||
if err != nil && !errors.Is(err, EACCES) {
|
|
||||||
return wrapErrSuffix(err,
|
|
||||||
fmt.Sprintf("cannot propagate flags to %q:", cur.Clean))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,15 +189,13 @@ func mountTmpfs(k syscallDispatcher, fsname, target string, flags uintptr, size
|
|||||||
// syscallDispatcher.mountTmpfs must not be called from this function
|
// syscallDispatcher.mountTmpfs must not be called from this function
|
||||||
|
|
||||||
if err := k.mkdirAll(target, parentPerm(perm)); err != nil {
|
if err := k.mkdirAll(target, parentPerm(perm)); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
opt := fmt.Sprintf("mode=%#o", perm)
|
opt := fmt.Sprintf("mode=%#o", perm)
|
||||||
if size > 0 {
|
if size > 0 {
|
||||||
opt += fmt.Sprintf(",size=%d", size)
|
opt += fmt.Sprintf(",size=%d", size)
|
||||||
}
|
}
|
||||||
return wrapErrSuffix(
|
return k.mount(fsname, target, FstypeTmpfs, flags, opt)
|
||||||
k.mount(fsname, target, FstypeTmpfs, flags, opt),
|
|
||||||
fmt.Sprintf("cannot mount tmpfs on %q:", target))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parentPerm(perm os.FileMode) os.FileMode {
|
func parentPerm(perm os.FileMode) os.FileMode {
|
||||||
|
|||||||
@ -5,32 +5,30 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
"hakurei.app/container/vfs"
|
"hakurei.app/container/vfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBindMount(t *testing.T) {
|
func TestBindMount(t *testing.T) {
|
||||||
checkSimple(t, "bindMount", []simpleTestCase{
|
checkSimple(t, "bindMount", []simpleTestCase{
|
||||||
{"mount", func(k syscallDispatcher) error {
|
{"mount", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
|
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"verbosef", 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)),
|
||||||
{"mount", expectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, errUnique},
|
}}, stub.UniqueError(0xbad)},
|
||||||
}}, wrapErrSuffix(errUnique, `cannot mount "/host/nix" on "/sysroot/nix":`)},
|
|
||||||
|
|
||||||
{"success ne", func(k syscallDispatcher) error {
|
{"success ne", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY, false)
|
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"verbosef", 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),
|
||||||
{"mount", expectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil},
|
call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil),
|
||||||
{"remount", expectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil},
|
|
||||||
}}, nil},
|
}}, nil},
|
||||||
|
|
||||||
{"success", func(k syscallDispatcher) error {
|
{"success", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
|
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"verbosef", expectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil},
|
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil),
|
||||||
{"mount", expectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil},
|
call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil),
|
||||||
{"remount", expectArgs{"/sysroot/nix", uintptr(1)}, nil, nil},
|
|
||||||
}}, nil},
|
}}, nil},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -81,138 +79,138 @@ func TestRemount(t *testing.T) {
|
|||||||
checkSimple(t, "remount", []simpleTestCase{
|
checkSimple(t, "remount", []simpleTestCase{
|
||||||
{"evalSymlinks", func(k syscallDispatcher) error {
|
{"evalSymlinks", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", errUnique},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)),
|
||||||
}}, wrapErrSelf(errUnique)},
|
}}, stub.UniqueError(6)},
|
||||||
|
|
||||||
{"open", func(k syscallDispatcher) error {
|
{"open", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, errUnique},
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, stub.UniqueError(5)),
|
||||||
}}, wrapErrSuffix(errUnique, `cannot open "/sysroot/nix":`)},
|
}}, &os.PathError{Op: "open", Path: "/sysroot/nix", Err: stub.UniqueError(5)}},
|
||||||
|
|
||||||
{"readlink", func(k syscallDispatcher) error {
|
{"readlink", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", errUnique},
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", stub.UniqueError(4)),
|
||||||
}}, wrapErrSelf(errUnique)},
|
}}, stub.UniqueError(4)},
|
||||||
|
|
||||||
{"close", func(k syscallDispatcher) error {
|
{"close", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
{"close", expectArgs{0xdeadbeef}, nil, errUnique},
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, stub.UniqueError(3)),
|
||||||
}}, wrapErrSuffix(errUnique, `cannot close "/sysroot/nix":`)},
|
}}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}},
|
||||||
|
|
||||||
{"mountinfo stale", func(k syscallDispatcher) error {
|
{"mountinfo no match", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil),
|
||||||
{"verbosef", expectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil},
|
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil),
|
||||||
{"open", expectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
call("open", stub.ExpectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil},
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil),
|
||||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
}}, msg.WrapErr(syscall.ESTALE, `mount point "/sysroot/.hakurei" never appeared in mountinfo`)},
|
}}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}},
|
||||||
|
|
||||||
{"mountinfo", func(k syscallDispatcher) error {
|
{"mountinfo", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil},
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil),
|
||||||
}}, wrapErrSuffix(vfs.ErrMountInfoFields, `cannot parse mountinfo:`)},
|
}}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}},
|
||||||
|
|
||||||
{"mount", func(k syscallDispatcher) error {
|
{"mount", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, errUnique},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, stub.UniqueError(2)),
|
||||||
}}, wrapErrSuffix(errUnique, `cannot remount "/sysroot/nix":`)},
|
}}, stub.UniqueError(2)},
|
||||||
|
|
||||||
{"mount propagate", func(k syscallDispatcher) error {
|
{"mount propagate", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, errUnique},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)),
|
||||||
}}, wrapErrSuffix(errUnique, `cannot propagate flags to "/sysroot/nix/.ro-store":`)},
|
}}, stub.UniqueError(1)},
|
||||||
|
|
||||||
{"success toplevel", func(k syscallDispatcher) error {
|
{"success toplevel", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/bin"}, "/sysroot/bin", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil),
|
||||||
{"open", expectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil},
|
call("open", stub.ExpectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil),
|
||||||
{"readlink", expectArgs{"/host/proc/self/fd/47806"}, "/sysroot/bin", nil},
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/47806"}, "/sysroot/bin", nil),
|
||||||
{"close", expectArgs{0xbabe}, nil, nil},
|
call("close", stub.ExpectArgs{0xbabe}, nil, nil),
|
||||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
}}, nil},
|
}}, nil},
|
||||||
|
|
||||||
{"success EACCES", func(k syscallDispatcher) error {
|
{"success EACCES", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
}}, nil},
|
}}, nil},
|
||||||
|
|
||||||
{"success no propagate", func(k syscallDispatcher) error {
|
{"success no propagate", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
}}, nil},
|
}}, nil},
|
||||||
|
|
||||||
{"success case sensitive", func(k syscallDispatcher) error {
|
{"success case sensitive", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
|
||||||
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
}}, nil},
|
}}, nil},
|
||||||
|
|
||||||
{"success", func(k syscallDispatcher) error {
|
{"success", func(k syscallDispatcher) error {
|
||||||
return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"evalSymlinks", expectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil},
|
call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil),
|
||||||
{"verbosef", expectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil},
|
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil),
|
||||||
{"open", expectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdeadbeef, nil},
|
call("open", stub.ExpectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdeadbeef, nil),
|
||||||
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
|
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
|
||||||
{"close", expectArgs{0xdeadbeef}, nil, nil},
|
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
|
||||||
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
|
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
|
||||||
}}, nil},
|
}}, nil},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -221,18 +219,18 @@ func TestRemountWithFlags(t *testing.T) {
|
|||||||
checkSimple(t, "remountWithFlags", []simpleTestCase{
|
checkSimple(t, "remountWithFlags", []simpleTestCase{
|
||||||
{"noop unmatched", func(k syscallDispatcher) error {
|
{"noop unmatched", func(k syscallDispatcher) error {
|
||||||
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
|
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"verbosef", expectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil},
|
call("verbosef", stub.ExpectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil),
|
||||||
}}, nil},
|
}}, nil},
|
||||||
|
|
||||||
{"noop", func(k syscallDispatcher) error {
|
{"noop", func(k syscallDispatcher) error {
|
||||||
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
|
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
|
||||||
}, nil, nil},
|
}, stub.Expect{}, nil},
|
||||||
|
|
||||||
{"success", func(k syscallDispatcher) error {
|
{"success", func(k syscallDispatcher) error {
|
||||||
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
|
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"mount", expectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil},
|
call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil),
|
||||||
}}, nil},
|
}}, nil},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -241,22 +239,22 @@ func TestMountTmpfs(t *testing.T) {
|
|||||||
checkSimple(t, "mountTmpfs", []simpleTestCase{
|
checkSimple(t, "mountTmpfs", []simpleTestCase{
|
||||||
{"mkdirAll", func(k syscallDispatcher) error {
|
{"mkdirAll", func(k syscallDispatcher) error {
|
||||||
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
|
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, errUnique},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)),
|
||||||
}}, wrapErrSelf(errUnique)},
|
}}, stub.UniqueError(0)},
|
||||||
|
|
||||||
{"success no size", func(k syscallDispatcher) error {
|
{"success no size", func(k syscallDispatcher) error {
|
||||||
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710)
|
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil),
|
||||||
{"mount", expectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil},
|
call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil),
|
||||||
}}, nil},
|
}}, nil},
|
||||||
|
|
||||||
{"success", func(k syscallDispatcher) error {
|
{"success", func(k syscallDispatcher) error {
|
||||||
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
|
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
|
||||||
}, [][]kexpect{{
|
}, stub.Expect{Calls: []stub.Call{
|
||||||
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil},
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil),
|
||||||
{"mount", expectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0700,size=1024"}, nil, nil},
|
call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0700,size=1024"}, nil, nil),
|
||||||
}}, nil},
|
}}, nil},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,24 +2,34 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MessageError is an error with a user-facing message.
|
||||||
|
type MessageError interface {
|
||||||
|
// Message returns a user-facing error message.
|
||||||
|
Message() string
|
||||||
|
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetErrorMessage returns whether an error implements [MessageError], and the message if it does.
|
||||||
|
func GetErrorMessage(err error) (string, bool) {
|
||||||
|
var e MessageError
|
||||||
|
if !errors.As(err, &e) || e == nil {
|
||||||
|
return zeroString, false
|
||||||
|
}
|
||||||
|
return e.Message(), true
|
||||||
|
}
|
||||||
|
|
||||||
type Msg interface {
|
type Msg interface {
|
||||||
IsVerbose() bool
|
IsVerbose() bool
|
||||||
Verbose(v ...any)
|
Verbose(v ...any)
|
||||||
Verbosef(format string, v ...any)
|
Verbosef(format string, v ...any)
|
||||||
WrapErr(err error, a ...any) error
|
|
||||||
PrintBaseErr(err error, fallback string)
|
|
||||||
|
|
||||||
Suspend()
|
Suspend()
|
||||||
Resume() bool
|
Resume() bool
|
||||||
|
|
||||||
BeforeExit()
|
BeforeExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,32 +47,21 @@ func (msg *DefaultMsg) Verbosef(format string, v ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkedWrappedErr implements error with strict checks for wrapped values.
|
|
||||||
type checkedWrappedErr struct {
|
|
||||||
err error
|
|
||||||
a []any
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *checkedWrappedErr) Error() string { return fmt.Sprintf("%v, a = %s", c.err, c.a) }
|
|
||||||
func (c *checkedWrappedErr) Is(err error) bool {
|
|
||||||
var concreteErr *checkedWrappedErr
|
|
||||||
if !errors.As(err, &concreteErr) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return reflect.DeepEqual(c, concreteErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *DefaultMsg) WrapErr(err error, a ...any) error {
|
|
||||||
// provide a mostly bulletproof path to bypass this behaviour in tests
|
|
||||||
if testing.Testing() && os.Getenv("GOPATH") != Nonexistent {
|
|
||||||
return &checkedWrappedErr{err, a}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println(a...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) }
|
|
||||||
|
|
||||||
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
|
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
|
||||||
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
|
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
|
||||||
func (msg *DefaultMsg) BeforeExit() {}
|
func (msg *DefaultMsg) BeforeExit() {}
|
||||||
|
|
||||||
|
// msg is the [Msg] implemented used by all exported [container] functions.
|
||||||
|
var msg Msg = new(DefaultMsg)
|
||||||
|
|
||||||
|
// GetOutput returns the current active [Msg] implementation.
|
||||||
|
func GetOutput() Msg { return msg }
|
||||||
|
|
||||||
|
// SetOutput replaces the current active [Msg] implementation.
|
||||||
|
func SetOutput(v Msg) {
|
||||||
|
if v == nil {
|
||||||
|
msg = new(DefaultMsg)
|
||||||
|
} else {
|
||||||
|
msg = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -9,13 +9,36 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
"hakurei.app/internal/hlog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDefaultMsg(t *testing.T) {
|
func TestMessageError(t *testing.T) {
|
||||||
// bypass WrapErr testing behaviour
|
testCases := []struct {
|
||||||
t.Setenv("GOPATH", container.Nonexistent)
|
name string
|
||||||
|
err error
|
||||||
|
want string
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{"nil", nil, "", false},
|
||||||
|
{"new", errors.New(":3"), "", false},
|
||||||
|
{"start", &container.StartError{
|
||||||
|
Step: "meow",
|
||||||
|
Err: syscall.ENOTRECOVERABLE,
|
||||||
|
}, "cannot meow: state not recoverable", true},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, ok := container.GetErrorMessage(tc.err)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("GetErrorMessage: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
if ok != tc.wantOk {
|
||||||
|
t.Errorf("GetErrorMessage: ok = %v, want %v", ok, tc.wantOk)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultMsg(t *testing.T) {
|
||||||
{
|
{
|
||||||
w := log.Writer()
|
w := log.Writer()
|
||||||
f := log.Flags()
|
f := log.Flags()
|
||||||
@ -48,21 +71,6 @@ func TestDefaultMsg(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("wrapErr", func(t *testing.T) {
|
|
||||||
buf := new(strings.Builder)
|
|
||||||
log.SetOutput(buf)
|
|
||||||
log.SetFlags(0)
|
|
||||||
if err := msg.WrapErr(syscall.EBADE, "\x00", "\x00"); err != syscall.EBADE {
|
|
||||||
t.Errorf("WrapErr: %v", err)
|
|
||||||
}
|
|
||||||
msg.PrintBaseErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:")
|
|
||||||
|
|
||||||
want := "\x00 \x00\ncannot cuddle cat: state not recoverable\n"
|
|
||||||
if buf.String() != want {
|
|
||||||
t.Errorf("WrapErr: %q, want %q", buf.String(), want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("inactive", func(t *testing.T) {
|
t.Run("inactive", func(t *testing.T) {
|
||||||
{
|
{
|
||||||
inactive := msg.Resume()
|
inactive := msg.Resume()
|
||||||
@ -83,25 +91,6 @@ func TestDefaultMsg(t *testing.T) {
|
|||||||
|
|
||||||
// the function is a noop
|
// the function is a noop
|
||||||
t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() })
|
t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() })
|
||||||
|
|
||||||
t.Run("checkedWrappedErr", func(t *testing.T) {
|
|
||||||
// temporarily re-enable testing behaviour
|
|
||||||
t.Setenv("GOPATH", "")
|
|
||||||
wrappedErr := msg.WrapErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:", syscall.ENOTRECOVERABLE)
|
|
||||||
|
|
||||||
t.Run("string", func(t *testing.T) {
|
|
||||||
want := "state not recoverable, a = [cannot cuddle cat: state not recoverable]"
|
|
||||||
if got := wrappedErr.Error(); got != want {
|
|
||||||
t.Errorf("Error: %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("bad concrete type", func(t *testing.T) {
|
|
||||||
if errors.Is(wrappedErr, syscall.ENOTRECOVERABLE) {
|
|
||||||
t.Error("incorrect type assertion")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type panicWriter struct{}
|
type panicWriter struct{}
|
||||||
@ -139,9 +128,6 @@ func (out *testOutput) Verbosef(format string, v ...any) {
|
|||||||
out.t.Logf(format, v...)
|
out.t.Logf(format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (out *testOutput) WrapErr(err error, a ...any) error { return hlog.WrapErr(err, a...) }
|
|
||||||
func (out *testOutput) PrintBaseErr(err error, fallback string) { hlog.PrintBaseError(err, fallback) }
|
|
||||||
|
|
||||||
func (out *testOutput) Suspend() {
|
func (out *testOutput) Suspend() {
|
||||||
if out.suspended.CompareAndSwap(false, true) {
|
if out.suspended.CompareAndSwap(false, true) {
|
||||||
out.Verbose("suspend called")
|
out.Verbose("suspend called")
|
||||||
@ -160,3 +146,39 @@ func (out *testOutput) Resume() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (out *testOutput) BeforeExit() { out.Verbose("beforeExit called") }
|
func (out *testOutput) BeforeExit() { out.Verbose("beforeExit called") }
|
||||||
|
|
||||||
|
func TestGetSetOutput(t *testing.T) {
|
||||||
|
{
|
||||||
|
out := container.GetOutput()
|
||||||
|
t.Cleanup(func() { container.SetOutput(out) })
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("default", func(t *testing.T) {
|
||||||
|
container.SetOutput(new(stubOutput))
|
||||||
|
if v, ok := container.GetOutput().(*container.DefaultMsg); ok {
|
||||||
|
t.Fatalf("SetOutput: got unexpected output %#v", v)
|
||||||
|
}
|
||||||
|
container.SetOutput(nil)
|
||||||
|
if _, ok := container.GetOutput().(*container.DefaultMsg); !ok {
|
||||||
|
t.Fatalf("SetOutput: got unexpected output %#v", container.GetOutput())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("stub", func(t *testing.T) {
|
||||||
|
container.SetOutput(new(stubOutput))
|
||||||
|
if _, ok := container.GetOutput().(*stubOutput); !ok {
|
||||||
|
t.Fatalf("SetOutput: got unexpected output %#v", container.GetOutput())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type stubOutput struct {
|
||||||
|
wrapF func(error, ...any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*stubOutput) IsVerbose() bool { panic("unreachable") }
|
||||||
|
func (*stubOutput) Verbose(...any) { panic("unreachable") }
|
||||||
|
func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") }
|
||||||
|
func (*stubOutput) Suspend() { panic("unreachable") }
|
||||||
|
func (*stubOutput) Resume() bool { panic("unreachable") }
|
||||||
|
func (*stubOutput) BeforeExit() { panic("unreachable") }
|
||||||
|
|||||||
@ -1,26 +1,77 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
var msg Msg = new(DefaultMsg)
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
func GetOutput() Msg { return msg }
|
const (
|
||||||
func SetOutput(v Msg) {
|
suspendBufInitial = 1 << 12
|
||||||
if v == nil {
|
suspendBufMax = 1 << 24
|
||||||
msg = new(DefaultMsg)
|
)
|
||||||
} else {
|
|
||||||
msg = v
|
// 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 wrapErrSuffix(err error, a ...any) error {
|
func (s *Suspendable) Write(p []byte) (n int, err error) {
|
||||||
if err == nil {
|
if !s.s.Load() {
|
||||||
return nil
|
return s.Downstream.Write(p)
|
||||||
}
|
}
|
||||||
return msg.WrapErr(err, append(a, err)...)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapErrSelf(err error) error {
|
// IsSuspended returns whether [Suspendable] is currently between a call to Suspend and Resume.
|
||||||
if err == nil {
|
func (s *Suspendable) IsSuspended() bool { return s.s.Load() }
|
||||||
return nil
|
|
||||||
|
// Suspend causes [Suspendable] to start withholding output in its buffer.
|
||||||
|
func (s *Suspendable) Suspend() bool { return s.s.CompareAndSwap(false, true) }
|
||||||
|
|
||||||
|
// Resume undoes the effect of Suspend and dumps the buffered into the downstream [io.Writer].
|
||||||
|
func (s *Suspendable) Resume() (resumed bool, dropped uintptr, n int64, err error) {
|
||||||
|
if s.s.CompareAndSwap(true, false) {
|
||||||
|
s.bufMu.Lock()
|
||||||
|
defer s.bufMu.Unlock()
|
||||||
|
|
||||||
|
resumed = true
|
||||||
|
dropped = uintptr(s.dropped)
|
||||||
|
|
||||||
|
s.dropped = 0
|
||||||
|
n, err = io.Copy(s.Downstream, &s.buf)
|
||||||
|
s.buf.Reset()
|
||||||
}
|
}
|
||||||
return msg.WrapErr(err, err.Error())
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,110 +1,155 @@
|
|||||||
package container
|
package container_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/container/stub"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetSetOutput(t *testing.T) {
|
func TestSuspendable(t *testing.T) {
|
||||||
{
|
// copied from output.go
|
||||||
out := GetOutput()
|
const suspendBufMax = 1 << 24
|
||||||
t.Cleanup(func() { SetOutput(out) })
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("default", func(t *testing.T) {
|
const (
|
||||||
SetOutput(new(stubOutput))
|
// equivalent to len(want.pt)
|
||||||
if v, ok := GetOutput().(*DefaultMsg); ok {
|
nSpecialPtEquiv = -iota - 1
|
||||||
t.Fatalf("SetOutput: got unexpected output %#v", v)
|
// equivalent to len(want.w)
|
||||||
}
|
nSpecialWEquiv
|
||||||
SetOutput(nil)
|
// suspends writer before executing test case, implies nSpecialWEquiv
|
||||||
if _, ok := GetOutput().(*DefaultMsg); !ok {
|
nSpecialSuspend
|
||||||
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
|
// offset: resume writer and measure against dump instead, implies nSpecialPtEquiv
|
||||||
}
|
nSpecialDump
|
||||||
})
|
)
|
||||||
|
|
||||||
t.Run("stub", func(t *testing.T) {
|
|
||||||
SetOutput(new(stubOutput))
|
|
||||||
if _, ok := GetOutput().(*stubOutput); !ok {
|
|
||||||
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWrapErr(t *testing.T) {
|
|
||||||
{
|
|
||||||
out := GetOutput()
|
|
||||||
t.Cleanup(func() { SetOutput(out) })
|
|
||||||
}
|
|
||||||
|
|
||||||
var wrapFp *func(error, ...any) error
|
|
||||||
s := new(stubOutput)
|
|
||||||
SetOutput(s)
|
|
||||||
wrapFp = &s.wrapF
|
|
||||||
|
|
||||||
|
// shares the same writer
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
f func(t *testing.T)
|
w, pt []byte
|
||||||
|
err error
|
||||||
wantErr error
|
wantErr error
|
||||||
wantA []any
|
n int
|
||||||
}{
|
}{
|
||||||
{"suffix nil", func(t *testing.T) {
|
{"simple", []byte{0xde, 0xad, 0xbe, 0xef}, []byte{0xde, 0xad, 0xbe, 0xef},
|
||||||
if err := wrapErrSuffix(nil, "\x00"); err != nil {
|
nil, nil, nSpecialPtEquiv},
|
||||||
t.Errorf("wrapErrSuffix: %v", err)
|
|
||||||
}
|
{"error", []byte{0xb, 0xad}, []byte{0xb, 0xad},
|
||||||
}, nil, nil},
|
stub.UniqueError(0), stub.UniqueError(0), nSpecialPtEquiv},
|
||||||
{"suffix val", func(t *testing.T) {
|
|
||||||
if err := wrapErrSuffix(syscall.ENOTRECOVERABLE, "\x00\x00"); err != syscall.ENOTRECOVERABLE {
|
{"suspend short", []byte{0}, nil,
|
||||||
t.Errorf("wrapErrSuffix: %v", err)
|
nil, nil, nSpecialSuspend},
|
||||||
}
|
{"sw short 0", []byte{0xca, 0xfe, 0xba, 0xbe}, nil,
|
||||||
}, syscall.ENOTRECOVERABLE, []any{"\x00\x00", syscall.ENOTRECOVERABLE}},
|
nil, nil, nSpecialWEquiv},
|
||||||
{"self nil", func(t *testing.T) {
|
{"sw short 1", []byte{0xff}, nil,
|
||||||
if err := wrapErrSelf(nil); err != nil {
|
nil, nil, nSpecialWEquiv},
|
||||||
t.Errorf("wrapErrSelf: %v", err)
|
{"resume short", nil, []byte{0, 0xca, 0xfe, 0xba, 0xbe, 0xff}, nil, nil,
|
||||||
}
|
nSpecialDump},
|
||||||
}, nil, nil},
|
|
||||||
{"self val", func(t *testing.T) {
|
{"long pt", bytes.Repeat([]byte{0xff}, suspendBufMax+1), bytes.Repeat([]byte{0xff}, suspendBufMax+1),
|
||||||
if err := wrapErrSelf(syscall.ENOTRECOVERABLE); err != syscall.ENOTRECOVERABLE {
|
nil, nil, nSpecialPtEquiv},
|
||||||
t.Errorf("wrapErrSelf: %v", err)
|
|
||||||
}
|
{"suspend fill", bytes.Repeat([]byte{0xfe}, suspendBufMax), nil,
|
||||||
}, syscall.ENOTRECOVERABLE, []any{"state not recoverable"}},
|
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},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var dw expectWriter
|
||||||
|
|
||||||
|
w := container.Suspendable{Downstream: &dw}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
// these share the same writer, so cannot be subtests
|
||||||
var (
|
t.Logf("writing step %q", tc.name)
|
||||||
gotErr error
|
dw.expect, dw.err = tc.pt, tc.err
|
||||||
gotA []any
|
|
||||||
)
|
|
||||||
*wrapFp = func(err error, a ...any) error { gotErr = err; gotA = a; return err }
|
|
||||||
|
|
||||||
tc.f(t)
|
var (
|
||||||
if gotErr != tc.wantErr {
|
gotN int
|
||||||
t.Errorf("WrapErr: err = %v, want %v", gotErr, tc.wantErr)
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(gotA, tc.wantA) {
|
wantN = len(tc.w)
|
||||||
t.Errorf("WrapErr: a = %v, want %v", gotA, tc.wantA)
|
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 {
|
// expectWriter compares Write calls to expect.
|
||||||
wrapF func(error, ...any) error
|
type expectWriter struct {
|
||||||
|
expect []byte
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*stubOutput) IsVerbose() bool { panic("unreachable") }
|
func (w *expectWriter) Write(p []byte) (n int, err error) {
|
||||||
func (*stubOutput) Verbose(...any) { panic("unreachable") }
|
defer func() { w.expect = nil }()
|
||||||
func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") }
|
|
||||||
func (*stubOutput) PrintBaseErr(error, string) { panic("unreachable") }
|
|
||||||
func (*stubOutput) Suspend() { panic("unreachable") }
|
|
||||||
func (*stubOutput) Resume() bool { panic("unreachable") }
|
|
||||||
func (*stubOutput) BeforeExit() { panic("unreachable") }
|
|
||||||
|
|
||||||
func (s *stubOutput) WrapErr(err error, v ...any) error {
|
n, err = len(p), w.err
|
||||||
if s.wrapF == nil {
|
if w.expect == nil {
|
||||||
panic("unreachable")
|
return 0, errors.New("unexpected call to Write: " + strconv.Quote(string(p)))
|
||||||
}
|
}
|
||||||
return s.wrapF(err, v...)
|
if string(p) != string(w.expect) {
|
||||||
|
return 0, errors.New("p = " + strconv.Quote(string(p)) + ", want " + strconv.Quote(string(w.expect)))
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,11 +8,6 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNotSet = errors.New("environment variable not set")
|
|
||||||
ErrFdFormat = errors.New("bad file descriptor representation")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
|
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
|
||||||
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
|
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
|
||||||
if r, w, err := os.Pipe(); err != nil {
|
if r, w, err := os.Pipe(); err != nil {
|
||||||
@ -24,19 +19,23 @@ func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrReceiveEnv = errors.New("environment variable not set")
|
||||||
|
)
|
||||||
|
|
||||||
// Receive retrieves setup fd from the environment and receives params.
|
// Receive retrieves setup fd from the environment and receives params.
|
||||||
func Receive(key string, e any, fdp *uintptr) (func() error, error) {
|
func Receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||||
var setup *os.File
|
var setup *os.File
|
||||||
|
|
||||||
if s, ok := os.LookupEnv(key); !ok {
|
if s, ok := os.LookupEnv(key); !ok {
|
||||||
return nil, ErrNotSet
|
return nil, ErrReceiveEnv
|
||||||
} else {
|
} else {
|
||||||
if fd, err := strconv.Atoi(s); err != nil {
|
if fd, err := strconv.Atoi(s); err != nil {
|
||||||
return nil, ErrFdFormat
|
return nil, errors.Unwrap(err)
|
||||||
} else {
|
} else {
|
||||||
setup = os.NewFile(uintptr(fd), "setup")
|
setup = os.NewFile(uintptr(fd), "setup")
|
||||||
if setup == nil {
|
if setup == nil {
|
||||||
return nil, syscall.EBADF
|
return nil, syscall.EDOM
|
||||||
}
|
}
|
||||||
if fdp != nil {
|
if fdp != nil {
|
||||||
*fdp = setup.Fd()
|
*fdp = setup.Fd()
|
||||||
|
|||||||
@ -29,8 +29,8 @@ func TestSetupReceive(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrNotSet) {
|
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrReceiveEnv) {
|
||||||
t.Errorf("Receive: error = %v, want %v", err, container.ErrNotSet)
|
t.Errorf("Receive: error = %v, want %v", err, container.ErrReceiveEnv)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -38,8 +38,8 @@ func TestSetupReceive(t *testing.T) {
|
|||||||
const key = "TEST_ENV_FORMAT"
|
const key = "TEST_ENV_FORMAT"
|
||||||
t.Setenv(key, "")
|
t.Setenv(key, "")
|
||||||
|
|
||||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrFdFormat) {
|
if _, err := container.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
|
||||||
t.Errorf("Receive: error = %v, want %v", err, container.ErrFdFormat)
|
t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -47,8 +47,8 @@ func TestSetupReceive(t *testing.T) {
|
|||||||
const key = "TEST_ENV_RANGE"
|
const key = "TEST_ENV_RANGE"
|
||||||
t.Setenv(key, "-1")
|
t.Setenv(key, "-1")
|
||||||
|
|
||||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EBADF) {
|
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
|
||||||
t.Errorf("Receive: error = %v, want %v", err, syscall.EBADF)
|
t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -103,30 +102,29 @@ func toHost(name string) string {
|
|||||||
|
|
||||||
func createFile(name string, perm, pperm os.FileMode, content []byte) error {
|
func createFile(name string, perm, pperm os.FileMode, content []byte) error {
|
||||||
if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
|
if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
|
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
if content != nil {
|
if content != nil {
|
||||||
_, err = f.Write(content)
|
_, err = f.Write(content)
|
||||||
}
|
}
|
||||||
return errors.Join(f.Close(), wrapErrSelf(err))
|
return errors.Join(f.Close(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureFile(name string, perm, pperm os.FileMode) error {
|
func ensureFile(name string, perm, pperm os.FileMode) error {
|
||||||
fi, err := os.Stat(name)
|
fi, err := os.Stat(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
}
|
}
|
||||||
return createFile(name, perm, pperm, nil)
|
return createFile(name, perm, pperm, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
|
if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
|
||||||
err = msg.WrapErr(syscall.EISDIR,
|
err = &os.PathError{Op: "ensure", Path: name, Err: syscall.EISDIR}
|
||||||
fmt.Sprintf("path %q is a directory", name))
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -147,15 +145,14 @@ func (p *procPaths) stdout() string { return p.self + "/fd/1" }
|
|||||||
func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
|
func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
|
||||||
func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
|
func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
|
||||||
if r, err := p.k.openNew(p.self + "/mountinfo"); err != nil {
|
if r, err := p.k.openNew(p.self + "/mountinfo"); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else {
|
} else {
|
||||||
d := vfs.NewMountInfoDecoder(r)
|
d := vfs.NewMountInfoDecoder(r)
|
||||||
err0 := f(d)
|
err0 := f(d)
|
||||||
if err = r.Close(); err != nil {
|
if err = r.Close(); err != nil {
|
||||||
return wrapErrSelf(err)
|
return err
|
||||||
} else if err = d.Err(); err != nil {
|
} else if err = d.Err(); err != nil {
|
||||||
return wrapErrSuffix(err,
|
return err
|
||||||
"cannot parse mountinfo:")
|
|
||||||
}
|
}
|
||||||
return err0
|
return err0
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
@ -56,20 +54,27 @@ func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment(
|
|||||||
|
|
||||||
func TestCreateFile(t *testing.T) {
|
func TestCreateFile(t *testing.T) {
|
||||||
t.Run("nonexistent", func(t *testing.T) {
|
t.Run("nonexistent", func(t *testing.T) {
|
||||||
if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{
|
t.Run("mkdir", func(t *testing.T) {
|
||||||
Op: "mkdir",
|
wantErr := &os.PathError{
|
||||||
Path: "/proc/nonexistent",
|
Op: "mkdir",
|
||||||
Err: syscall.ENOENT,
|
Path: "/proc/nonexistent",
|
||||||
})) {
|
Err: syscall.ENOENT,
|
||||||
t.Errorf("createFile: error = %v", err)
|
}
|
||||||
}
|
if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
|
||||||
if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{
|
t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
|
||||||
Op: "open",
|
}
|
||||||
Path: "/proc/nonexistent",
|
})
|
||||||
Err: syscall.ENOENT,
|
|
||||||
})) {
|
t.Run("open", func(t *testing.T) {
|
||||||
t.Errorf("createFile: error = %v", err)
|
wantErr := &os.PathError{
|
||||||
}
|
Op: "open",
|
||||||
|
Path: "/proc/nonexistent",
|
||||||
|
Err: syscall.ENOENT,
|
||||||
|
}
|
||||||
|
if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
|
||||||
|
t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("touch", func(t *testing.T) {
|
t.Run("touch", func(t *testing.T) {
|
||||||
@ -120,13 +125,13 @@ func TestEnsureFile(t *testing.T) {
|
|||||||
t.Fatalf("Chmod: error = %v", err)
|
t.Fatalf("Chmod: error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantErr := wrapErrSelf(&os.PathError{
|
wantErr := &os.PathError{
|
||||||
Op: "stat",
|
Op: "stat",
|
||||||
Path: pathname,
|
Path: pathname,
|
||||||
Err: syscall.EACCES,
|
Err: syscall.EACCES,
|
||||||
})
|
}
|
||||||
if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) {
|
if err := ensureFile(pathname, 0644, 0755); !reflect.DeepEqual(err, wantErr) {
|
||||||
t.Errorf("ensureFile: error = %v, want %v", err, wantErr)
|
t.Errorf("ensureFile: error = %#v, want %#v", err, wantErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Chmod(tempDir, 0755); err != nil {
|
if err := os.Chmod(tempDir, 0755); err != nil {
|
||||||
@ -136,9 +141,9 @@ func TestEnsureFile(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("directory", func(t *testing.T) {
|
t.Run("directory", func(t *testing.T) {
|
||||||
pathname := t.TempDir()
|
pathname := t.TempDir()
|
||||||
wantErr := msg.WrapErr(syscall.EISDIR, fmt.Sprintf("path %q is a directory", pathname))
|
wantErr := &os.PathError{Op: "ensure", Path: pathname, Err: syscall.EISDIR}
|
||||||
if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) {
|
if err := ensureFile(pathname, 0644, 0755); !reflect.DeepEqual(err, wantErr) {
|
||||||
t.Errorf("ensureFile: error = %v, want %v", err, wantErr)
|
t.Errorf("ensureFile: error = %#v, want %#v", err, wantErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -177,12 +182,12 @@ func TestProcPaths(t *testing.T) {
|
|||||||
t.Run("mountinfo", func(t *testing.T) {
|
t.Run("mountinfo", func(t *testing.T) {
|
||||||
t.Run("nonexistent", func(t *testing.T) {
|
t.Run("nonexistent", func(t *testing.T) {
|
||||||
nonexistentProc := newProcPaths(direct{}, t.TempDir())
|
nonexistentProc := newProcPaths(direct{}, t.TempDir())
|
||||||
wantErr := wrapErrSelf(&os.PathError{
|
wantErr := &os.PathError{
|
||||||
Op: "open",
|
Op: "open",
|
||||||
Path: nonexistentProc.self + "/mountinfo",
|
Path: nonexistentProc.self + "/mountinfo",
|
||||||
Err: syscall.ENOENT,
|
Err: syscall.ENOENT,
|
||||||
})
|
}
|
||||||
if err := nonexistentProc.mountinfo(func(*vfs.MountInfoDecoder) error { return syscall.EINVAL }); !errors.Is(err, wantErr) {
|
if err := nonexistentProc.mountinfo(func(*vfs.MountInfoDecoder) error { return syscall.EINVAL }); !reflect.DeepEqual(err, wantErr) {
|
||||||
t.Errorf("mountinfo: error = %v, want %v", err, wantErr)
|
t.Errorf("mountinfo: error = %v, want %v", err, wantErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -217,11 +222,11 @@ func TestProcPaths(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("closed", func(t *testing.T) {
|
t.Run("closed", func(t *testing.T) {
|
||||||
p := newProcPaths(direct{}, tempDir)
|
p := newProcPaths(direct{}, tempDir)
|
||||||
wantErr := wrapErrSelf(&os.PathError{
|
wantErr := &os.PathError{
|
||||||
Op: "close",
|
Op: "close",
|
||||||
Path: p.self + "/mountinfo",
|
Path: p.self + "/mountinfo",
|
||||||
Err: os.ErrClosed,
|
Err: os.ErrClosed,
|
||||||
})
|
}
|
||||||
if err := p.mountinfo(func(d *vfs.MountInfoDecoder) error {
|
if err := p.mountinfo(func(d *vfs.MountInfoDecoder) error {
|
||||||
v := reflect.ValueOf(d).Elem().FieldByName("s").Elem().FieldByName("r")
|
v := reflect.ValueOf(d).Elem().FieldByName("s").Elem().FieldByName("r")
|
||||||
v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr()))
|
v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr()))
|
||||||
@ -231,8 +236,8 @@ func TestProcPaths(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
return f.Close()
|
return f.Close()
|
||||||
}
|
}
|
||||||
}); !errors.Is(err, wantErr) {
|
}); !reflect.DeepEqual(err, wantErr) {
|
||||||
t.Errorf("mountinfo: error = %v, want %v", err, wantErr)
|
t.Errorf("mountinfo: error = %#v, want %#v", err, wantErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -242,8 +247,8 @@ func TestProcPaths(t *testing.T) {
|
|||||||
t.Fatalf("WriteFile: error = %v", err)
|
t.Fatalf("WriteFile: error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantErr := wrapErrSuffix(vfs.ErrMountInfoFields, "cannot parse mountinfo:")
|
wantErr := &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}
|
||||||
if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !errors.Is(err, wantErr) {
|
if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !reflect.DeepEqual(err, wantErr) {
|
||||||
t.Fatalf("mountinfo: error = %v, want %v", err, wantErr)
|
t.Fatalf("mountinfo: error = %v, want %v", err, wantErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
37
container/stub/call.go
Normal file
37
container/stub/call.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package stub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExpectArgs is an array primarily for storing expected function arguments.
|
||||||
|
// Its actual use is defined by the implementation.
|
||||||
|
type ExpectArgs = [5]any
|
||||||
|
|
||||||
|
// An Expect stores expected calls of a goroutine.
|
||||||
|
type Expect struct {
|
||||||
|
Calls []Call
|
||||||
|
|
||||||
|
// Tracks are handed out to descendant goroutines in order.
|
||||||
|
Tracks []Expect
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Call holds expected arguments of a function call and its outcome.
|
||||||
|
type Call struct {
|
||||||
|
// Name is the function Name of this call. Must be unique.
|
||||||
|
Name string
|
||||||
|
// Args are the expected arguments of this Call.
|
||||||
|
Args ExpectArgs
|
||||||
|
// Ret is the return value of this Call.
|
||||||
|
Ret any
|
||||||
|
// Err is the returned error of this Call.
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns [Call.Err] if all arguments are true, or [ErrCheck] otherwise.
|
||||||
|
func (k *Call) Error(ok ...bool) error {
|
||||||
|
if !slices.Contains(ok, false) {
|
||||||
|
return k.Err
|
||||||
|
}
|
||||||
|
return ErrCheck
|
||||||
|
}
|
||||||
23
container/stub/call_test.go
Normal file
23
container/stub/call_test.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package stub_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCallError(t *testing.T) {
|
||||||
|
t.Run("contains false", func(t *testing.T) {
|
||||||
|
if err := new(stub.Call).Error(true, false, true); !reflect.DeepEqual(err, stub.ErrCheck) {
|
||||||
|
t.Errorf("Error: %#v, want %#v", err, stub.ErrCheck)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("passthrough", func(t *testing.T) {
|
||||||
|
wantErr := stub.UniqueError(0xbabe)
|
||||||
|
if err := (&stub.Call{Err: wantErr}).Error(true); !reflect.DeepEqual(err, wantErr) {
|
||||||
|
t.Errorf("Error: %#v, want %#v", err, wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
25
container/stub/errors.go
Normal file
25
container/stub/errors.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package stub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCheck = errors.New("one or more arguments did not match")
|
||||||
|
)
|
||||||
|
|
||||||
|
// UniqueError is an error that only equivalates to other [UniqueError] with the same magic value.
|
||||||
|
type UniqueError uintptr
|
||||||
|
|
||||||
|
func (e UniqueError) Error() string {
|
||||||
|
return "unique error " + strconv.Itoa(int(e)) + " injected by the test suite"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UniqueError) Is(target error) bool {
|
||||||
|
var u UniqueError
|
||||||
|
if !errors.As(target, &u) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return e == u
|
||||||
|
}
|
||||||
35
container/stub/errors_test.go
Normal file
35
container/stub/errors_test.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package stub_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUniqueError(t *testing.T) {
|
||||||
|
t.Run("format", func(t *testing.T) {
|
||||||
|
want := "unique error 2989 injected by the test suite"
|
||||||
|
if got := stub.UniqueError(0xbad).Error(); got != want {
|
||||||
|
t.Errorf("Error: %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("is", func(t *testing.T) {
|
||||||
|
t.Run("type", func(t *testing.T) {
|
||||||
|
if errors.Is(stub.UniqueError(0), syscall.ENOTRECOVERABLE) {
|
||||||
|
t.Error("Is: unexpected true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("val", func(t *testing.T) {
|
||||||
|
if errors.Is(stub.UniqueError(0), stub.UniqueError(1)) {
|
||||||
|
t.Error("Is: unexpected true")
|
||||||
|
}
|
||||||
|
if !errors.Is(stub.UniqueError(0xbad), stub.UniqueError(0xbad)) {
|
||||||
|
t.Error("Is: unexpected false")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
44
container/stub/exit.go
Normal file
44
container/stub/exit.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package stub
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// PanicExit is a magic panic value treated as a simulated exit.
|
||||||
|
const PanicExit = 0xdeadbeef
|
||||||
|
|
||||||
|
const (
|
||||||
|
panicFailNow = 0xcafe0000 + iota
|
||||||
|
panicFatal
|
||||||
|
panicFatalf
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleExit must be deferred before calling with the stub.
|
||||||
|
func HandleExit(t testing.TB) {
|
||||||
|
switch r := recover(); r {
|
||||||
|
case PanicExit:
|
||||||
|
break
|
||||||
|
|
||||||
|
case panicFailNow:
|
||||||
|
t.FailNow()
|
||||||
|
|
||||||
|
case panicFatal, panicFatalf, nil:
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleExitNew handles exits from goroutines created by [Stub.New].
|
||||||
|
func handleExitNew(t testing.TB) {
|
||||||
|
switch r := recover(); r {
|
||||||
|
case PanicExit, panicFatal, panicFatalf, nil:
|
||||||
|
break
|
||||||
|
|
||||||
|
case panicFailNow:
|
||||||
|
t.Fail()
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
93
container/stub/exit_test.go
Normal file
93
container/stub/exit_test.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package stub_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
_ "unsafe"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:linkname handleExitNew hakurei.app/container/stub.handleExitNew
|
||||||
|
func handleExitNew(_ testing.TB)
|
||||||
|
|
||||||
|
// overrideTFailNow overrides the Fail and FailNow method.
|
||||||
|
type overrideTFailNow struct {
|
||||||
|
*testing.T
|
||||||
|
failNow bool
|
||||||
|
fail bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *overrideTFailNow) FailNow() {
|
||||||
|
if o.failNow {
|
||||||
|
o.Errorf("attempted to FailNow twice")
|
||||||
|
}
|
||||||
|
o.failNow = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *overrideTFailNow) Fail() {
|
||||||
|
if o.fail {
|
||||||
|
o.Errorf("attempted to Fail twice")
|
||||||
|
}
|
||||||
|
o.fail = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleExit(t *testing.T) {
|
||||||
|
t.Run("exit", func(t *testing.T) {
|
||||||
|
defer stub.HandleExit(t)
|
||||||
|
panic(stub.PanicExit)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("goexit", func(t *testing.T) {
|
||||||
|
t.Run("FailNow", func(t *testing.T) {
|
||||||
|
ot := &overrideTFailNow{T: t}
|
||||||
|
defer func() {
|
||||||
|
if !ot.failNow {
|
||||||
|
t.Errorf("FailNow was never called")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer stub.HandleExit(ot)
|
||||||
|
panic(0xcafe0000)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Fail", func(t *testing.T) {
|
||||||
|
ot := &overrideTFailNow{T: t}
|
||||||
|
defer func() {
|
||||||
|
if !ot.fail {
|
||||||
|
t.Errorf("Fail was never called")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer handleExitNew(ot)
|
||||||
|
panic(0xcafe0000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
defer stub.HandleExit(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("passthrough", func(t *testing.T) {
|
||||||
|
t.Run("toplevel", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
want := 0xcafebabe
|
||||||
|
if r := recover(); r != want {
|
||||||
|
t.Errorf("recover: %v, want %v", r, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
defer stub.HandleExit(t)
|
||||||
|
panic(0xcafebabe)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("new", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
want := 0xcafe
|
||||||
|
if r := recover(); r != want {
|
||||||
|
t.Errorf("recover: %v, want %v", r, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
defer handleExitNew(t)
|
||||||
|
panic(0xcafe)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
148
container/stub/stub.go
Normal file
148
container/stub/stub.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// Package stub provides function call level stubbing and validation
|
||||||
|
// for library functions that are impossible to check otherwise.
|
||||||
|
package stub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// this should prevent stub from being inadvertently imported outside tests
|
||||||
|
var _ = func() {
|
||||||
|
if !testing.Testing() {
|
||||||
|
panic("stub imported while not in a test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// A CallSeparator denotes an injected separation between two groups of calls.
|
||||||
|
CallSeparator = "\x00"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Stub is a collection of tracks of expected calls.
|
||||||
|
type Stub[K any] struct {
|
||||||
|
testing.TB
|
||||||
|
|
||||||
|
// makeK creates a new K for a descendant [Stub].
|
||||||
|
// This function may be called concurrently.
|
||||||
|
makeK func(s *Stub[K]) K
|
||||||
|
|
||||||
|
// want is a hierarchy of expected calls.
|
||||||
|
want Expect
|
||||||
|
// pos is the current position in [Expect.Calls].
|
||||||
|
pos int
|
||||||
|
// goroutine counts the number of goroutines created by this [Stub].
|
||||||
|
goroutine int
|
||||||
|
// sub stores the addresses of descendant [Stub] created by New.
|
||||||
|
sub []*Stub[K]
|
||||||
|
// wg waits for all descendants to complete.
|
||||||
|
wg *sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a root [Stub].
|
||||||
|
func New[K any](tb testing.TB, makeK func(s *Stub[K]) K, want Expect) *Stub[K] {
|
||||||
|
return &Stub[K]{TB: tb, makeK: makeK, want: want, wg: new(sync.WaitGroup)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stub[K]) FailNow() { panic(panicFailNow) }
|
||||||
|
func (s *Stub[K]) Fatal(args ...any) { s.Error(args...); panic(panicFatal) }
|
||||||
|
func (s *Stub[K]) Fatalf(format string, args ...any) { s.Errorf(format, args...); panic(panicFatalf) }
|
||||||
|
func (s *Stub[K]) SkipNow() { panic("invalid call to SkipNow") }
|
||||||
|
func (s *Stub[K]) Skip(...any) { panic("invalid call to Skip") }
|
||||||
|
func (s *Stub[K]) Skipf(string, ...any) { panic("invalid call to Skipf") }
|
||||||
|
|
||||||
|
// New calls f in a new goroutine
|
||||||
|
func (s *Stub[K]) New(f func(k K)) {
|
||||||
|
s.Helper()
|
||||||
|
|
||||||
|
s.Expects("New")
|
||||||
|
if len(s.want.Tracks) <= s.goroutine {
|
||||||
|
s.Fatal("New: track overrun")
|
||||||
|
}
|
||||||
|
ds := &Stub[K]{TB: s.TB, makeK: s.makeK, want: s.want.Tracks[s.goroutine], wg: s.wg}
|
||||||
|
s.goroutine++
|
||||||
|
s.sub = append(s.sub, ds)
|
||||||
|
s.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
s.Helper()
|
||||||
|
|
||||||
|
defer s.wg.Done()
|
||||||
|
defer handleExitNew(s.TB)
|
||||||
|
f(s.makeK(ds))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pos returns the current position of [Stub] in its [Expect.Calls]
|
||||||
|
func (s *Stub[K]) Pos() int { return s.pos }
|
||||||
|
|
||||||
|
// Len returns the length of [Expect.Calls].
|
||||||
|
func (s *Stub[K]) Len() int { return len(s.want.Calls) }
|
||||||
|
|
||||||
|
// VisitIncomplete calls f on an incomplete s and all its descendants.
|
||||||
|
func (s *Stub[K]) VisitIncomplete(f func(s *Stub[K])) {
|
||||||
|
s.Helper()
|
||||||
|
s.wg.Wait()
|
||||||
|
|
||||||
|
if s.want.Calls != nil && len(s.want.Calls) != s.pos {
|
||||||
|
f(s)
|
||||||
|
}
|
||||||
|
for _, ds := range s.sub {
|
||||||
|
ds.VisitIncomplete(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expects checks the name of and returns the current [Call] and advances pos.
|
||||||
|
func (s *Stub[K]) Expects(name string) (expect *Call) {
|
||||||
|
s.Helper()
|
||||||
|
|
||||||
|
if len(s.want.Calls) == s.pos {
|
||||||
|
s.Fatal("Expects: advancing beyond expected calls")
|
||||||
|
}
|
||||||
|
expect = &s.want.Calls[s.pos]
|
||||||
|
if name != expect.Name {
|
||||||
|
if expect.Name == CallSeparator {
|
||||||
|
s.Fatalf("Expects: func = %s, separator overrun", name)
|
||||||
|
}
|
||||||
|
if name == CallSeparator {
|
||||||
|
s.Fatalf("Expects: separator, want %s", expect.Name)
|
||||||
|
}
|
||||||
|
s.Fatalf("Expects: func = %s, want %s", name, expect.Name)
|
||||||
|
}
|
||||||
|
s.pos++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckArg checks an argument comparable with the == operator. Avoid using this with pointers.
|
||||||
|
func CheckArg[T comparable, K any](s *Stub[K], arg string, got T, n int) bool {
|
||||||
|
s.Helper()
|
||||||
|
|
||||||
|
pos := s.pos - 1
|
||||||
|
if pos < 0 || pos >= len(s.want.Calls) {
|
||||||
|
panic("invalid call to CheckArg")
|
||||||
|
}
|
||||||
|
expect := s.want.Calls[pos]
|
||||||
|
want, ok := expect.Args[n].(T)
|
||||||
|
if !ok || got != want {
|
||||||
|
s.Errorf("%s: %s = %#v, want %#v (%d)", expect.Name, arg, got, want, pos)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckArgReflect checks an argument of any type.
|
||||||
|
func CheckArgReflect[K any](s *Stub[K], arg string, got any, n int) bool {
|
||||||
|
s.Helper()
|
||||||
|
|
||||||
|
pos := s.pos - 1
|
||||||
|
if pos < 0 || pos >= len(s.want.Calls) {
|
||||||
|
panic("invalid call to CheckArgReflect")
|
||||||
|
}
|
||||||
|
expect := s.want.Calls[pos]
|
||||||
|
want := expect.Args[n]
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
s.Errorf("%s: %s = %#v, want %#v (%d)", expect.Name, arg, got, want, pos)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
296
container/stub/stub_test.go
Normal file
296
container/stub/stub_test.go
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
package stub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// stubHolder embeds [Stub].
|
||||||
|
type stubHolder struct{ *Stub[stubHolder] }
|
||||||
|
|
||||||
|
// overrideT allows some methods of [testing.T] to be overridden.
|
||||||
|
type overrideT struct {
|
||||||
|
*testing.T
|
||||||
|
|
||||||
|
error atomic.Pointer[func(args ...any)]
|
||||||
|
errorf atomic.Pointer[func(format string, args ...any)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *overrideT) Error(args ...any) {
|
||||||
|
fp := t.error.Load()
|
||||||
|
if fp == nil || *fp == nil {
|
||||||
|
t.T.Error(args...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
(*fp)(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *overrideT) Errorf(format string, args ...any) {
|
||||||
|
fp := t.errorf.Load()
|
||||||
|
if fp == nil || *fp == nil {
|
||||||
|
t.T.Errorf(format, args...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
(*fp)(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStub(t *testing.T) {
|
||||||
|
t.Run("goexit", func(t *testing.T) {
|
||||||
|
t.Run("FailNow", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != panicFailNow {
|
||||||
|
t.Errorf("recover: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
new(stubHolder).FailNow()
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SkipNow", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
want := "invalid call to SkipNow"
|
||||||
|
if r := recover(); r != want {
|
||||||
|
t.Errorf("recover: %v, want %v", r, want)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
new(stubHolder).SkipNow()
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Skip", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
want := "invalid call to Skip"
|
||||||
|
if r := recover(); r != want {
|
||||||
|
t.Errorf("recover: %v, want %v", r, want)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
new(stubHolder).Skip()
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Skipf", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
want := "invalid call to Skipf"
|
||||||
|
if r := recover(); r != want {
|
||||||
|
t.Errorf("recover: %v, want %v", r, want)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
new(stubHolder).Skipf("")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("new", func(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
|
||||||
|
{"New", ExpectArgs{}, nil, nil},
|
||||||
|
}, Tracks: []Expect{{Calls: []Call{
|
||||||
|
{"done", ExpectArgs{0xbabe}, nil, nil},
|
||||||
|
}}}})
|
||||||
|
|
||||||
|
s.New(func(k stubHolder) {
|
||||||
|
expect := k.Expects("done")
|
||||||
|
if expect.Name != "done" {
|
||||||
|
t.Errorf("New: Name = %s, want done", expect.Name)
|
||||||
|
}
|
||||||
|
if expect.Args != (ExpectArgs{0xbabe}) {
|
||||||
|
t.Errorf("New: Args = %#v", expect.Args)
|
||||||
|
}
|
||||||
|
if expect.Ret != nil {
|
||||||
|
t.Errorf("New: Ret = %#v", expect.Ret)
|
||||||
|
}
|
||||||
|
if expect.Err != nil {
|
||||||
|
t.Errorf("New: Err = %#v", expect.Err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if pos := s.Pos(); pos != 1 {
|
||||||
|
t.Errorf("Pos: %d, want 1", pos)
|
||||||
|
}
|
||||||
|
if l := s.Len(); l != 1 {
|
||||||
|
t.Errorf("Len: %d, want 1", l)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.VisitIncomplete(func(s *Stub[stubHolder]) { panic("unreachable") })
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("overrun", func(t *testing.T) {
|
||||||
|
ot := &overrideT{T: t}
|
||||||
|
ot.error.Store(checkError(t, "New: track overrun"))
|
||||||
|
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
|
||||||
|
{"New", ExpectArgs{}, nil, nil},
|
||||||
|
{"panic", ExpectArgs{"unreachable"}, nil, nil},
|
||||||
|
}})
|
||||||
|
func() { defer HandleExit(t); s.New(func(k stubHolder) { panic("unreachable") }) }()
|
||||||
|
|
||||||
|
var visit int
|
||||||
|
s.VisitIncomplete(func(s *Stub[stubHolder]) {
|
||||||
|
visit++
|
||||||
|
if visit > 1 {
|
||||||
|
panic("unexpected visit count")
|
||||||
|
}
|
||||||
|
|
||||||
|
want := Call{"panic", ExpectArgs{"unreachable"}, nil, nil}
|
||||||
|
if got := s.want.Calls[s.pos]; !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("VisitIncomplete: %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("expects", func(t *testing.T) {
|
||||||
|
t.Run("overrun", func(t *testing.T) {
|
||||||
|
ot := &overrideT{T: t}
|
||||||
|
ot.error.Store(checkError(t, "Expects: advancing beyond expected calls"))
|
||||||
|
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
|
||||||
|
func() { defer HandleExit(t); s.Expects("unreachable") }()
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("separator", func(t *testing.T) {
|
||||||
|
t.Run("overrun", func(t *testing.T) {
|
||||||
|
ot := &overrideT{T: t}
|
||||||
|
ot.errorf.Store(checkErrorf(t, "Expects: func = %s, separator overrun", "meow"))
|
||||||
|
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
|
||||||
|
{CallSeparator, ExpectArgs{}, nil, nil},
|
||||||
|
}})
|
||||||
|
func() { defer HandleExit(t); s.Expects("meow") }()
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("mismatch", func(t *testing.T) {
|
||||||
|
ot := &overrideT{T: t}
|
||||||
|
ot.errorf.Store(checkErrorf(t, "Expects: separator, want %s", "panic"))
|
||||||
|
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
|
||||||
|
{"panic", ExpectArgs{}, nil, nil},
|
||||||
|
}})
|
||||||
|
func() { defer HandleExit(t); s.Expects(CallSeparator) }()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("mismatch", func(t *testing.T) {
|
||||||
|
ot := &overrideT{T: t}
|
||||||
|
ot.errorf.Store(checkErrorf(t, "Expects: func = %s, want %s", "meow", "nya"))
|
||||||
|
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
|
||||||
|
{"nya", ExpectArgs{}, nil, nil},
|
||||||
|
}})
|
||||||
|
func() { defer HandleExit(t); s.Expects("meow") }()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckArg(t *testing.T) {
|
||||||
|
t.Run("oob negative", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
want := "invalid call to CheckArg"
|
||||||
|
if r := recover(); r != want {
|
||||||
|
t.Errorf("recover: %v, want %v", r, want)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
|
||||||
|
CheckArg(s, "unreachable", struct{}{}, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
ot := &overrideT{T: t}
|
||||||
|
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
|
||||||
|
{"panic", ExpectArgs{PanicExit}, nil, nil},
|
||||||
|
{"meow", ExpectArgs{-1}, nil, nil},
|
||||||
|
}})
|
||||||
|
t.Run("match", func(t *testing.T) {
|
||||||
|
s.Expects("panic")
|
||||||
|
if !CheckArg(s, "v", PanicExit, 0) {
|
||||||
|
t.Errorf("CheckArg: unexpected false")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("mismatch", func(t *testing.T) {
|
||||||
|
defer HandleExit(t)
|
||||||
|
s.Expects("meow")
|
||||||
|
ot.errorf.Store(checkErrorf(t, "%s: %s = %#v, want %#v (%d)", "meow", "time", 0, -1, 1))
|
||||||
|
if CheckArg(s, "time", 0, 0) {
|
||||||
|
t.Errorf("CheckArg: unexpected true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("oob", func(t *testing.T) {
|
||||||
|
s.pos++
|
||||||
|
defer func() {
|
||||||
|
want := "invalid call to CheckArg"
|
||||||
|
if r := recover(); r != want {
|
||||||
|
t.Errorf("recover: %v, want %v", r, want)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
CheckArg(s, "unreachable", struct{}{}, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckArgReflect(t *testing.T) {
|
||||||
|
t.Run("oob lower", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
want := "invalid call to CheckArgReflect"
|
||||||
|
if r := recover(); r != want {
|
||||||
|
t.Errorf("recover: %v, want %v", r, want)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
|
||||||
|
CheckArgReflect(s, "unreachable", struct{}{}, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
ot := &overrideT{T: t}
|
||||||
|
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
|
||||||
|
{"panic", ExpectArgs{PanicExit}, nil, nil},
|
||||||
|
{"meow", ExpectArgs{-1}, nil, nil},
|
||||||
|
}})
|
||||||
|
t.Run("match", func(t *testing.T) {
|
||||||
|
s.Expects("panic")
|
||||||
|
if !CheckArgReflect(s, "v", PanicExit, 0) {
|
||||||
|
t.Errorf("CheckArgReflect: unexpected false")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("mismatch", func(t *testing.T) {
|
||||||
|
defer HandleExit(t)
|
||||||
|
s.Expects("meow")
|
||||||
|
ot.errorf.Store(checkErrorf(t, "%s: %s = %#v, want %#v (%d)", "meow", "time", 0, -1, 1))
|
||||||
|
if CheckArgReflect(s, "time", 0, 0) {
|
||||||
|
t.Errorf("CheckArgReflect: unexpected true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("oob", func(t *testing.T) {
|
||||||
|
s.pos++
|
||||||
|
defer func() {
|
||||||
|
want := "invalid call to CheckArgReflect"
|
||||||
|
if r := recover(); r != want {
|
||||||
|
t.Errorf("recover: %v, want %v", r, want)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
CheckArgReflect(s, "unreachable", struct{}{}, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkError(t *testing.T, wantArgs ...any) *func(args ...any) {
|
||||||
|
var called bool
|
||||||
|
f := func(args ...any) {
|
||||||
|
if called {
|
||||||
|
panic("invalid call to error")
|
||||||
|
}
|
||||||
|
called = true
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(args, wantArgs) {
|
||||||
|
t.Errorf("Error: %#v, want %#v", args, wantArgs)
|
||||||
|
}
|
||||||
|
panic(PanicExit)
|
||||||
|
}
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkErrorf(t *testing.T, wantFormat string, wantArgs ...any) *func(format string, args ...any) {
|
||||||
|
var called bool
|
||||||
|
f := func(format string, args ...any) {
|
||||||
|
if called {
|
||||||
|
panic("invalid call to errorf")
|
||||||
|
}
|
||||||
|
called = true
|
||||||
|
|
||||||
|
if format != wantFormat {
|
||||||
|
t.Errorf("Errorf: format = %q, want %q", format, wantFormat)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(args, wantArgs) {
|
||||||
|
t.Errorf("Errorf: args = %#v, want %#v", args, wantArgs)
|
||||||
|
}
|
||||||
|
panic(PanicExit)
|
||||||
|
}
|
||||||
|
return &f
|
||||||
|
}
|
||||||
@ -24,6 +24,32 @@ var (
|
|||||||
ErrMountInfoSep = errors.New("bad optional fields separator")
|
ErrMountInfoSep = errors.New("bad optional fields separator")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DecoderError struct {
|
||||||
|
Op string
|
||||||
|
Line int
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DecoderError) Unwrap() error { return e.Err }
|
||||||
|
func (e *DecoderError) Error() string {
|
||||||
|
var s string
|
||||||
|
|
||||||
|
var numError *strconv.NumError
|
||||||
|
switch {
|
||||||
|
case errors.As(e.Err, &numError) && numError != nil:
|
||||||
|
s = "numeric field " + strconv.Quote(numError.Num) + " " + numError.Err.Error()
|
||||||
|
|
||||||
|
default:
|
||||||
|
s = e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var atLine string
|
||||||
|
if e.Line >= 0 {
|
||||||
|
atLine = " at line " + strconv.Itoa(e.Line)
|
||||||
|
}
|
||||||
|
return e.Op + " mountinfo" + atLine + ": " + s
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream.
|
// A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream.
|
||||||
MountInfoDecoder struct {
|
MountInfoDecoder struct {
|
||||||
@ -32,6 +58,7 @@ type (
|
|||||||
|
|
||||||
current *MountInfo
|
current *MountInfo
|
||||||
parseErr error
|
parseErr error
|
||||||
|
curLine int
|
||||||
complete bool
|
complete bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,9 +159,12 @@ func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] {
|
|||||||
|
|
||||||
func (d *MountInfoDecoder) Err() error {
|
func (d *MountInfoDecoder) Err() error {
|
||||||
if err := d.s.Err(); err != nil {
|
if err := d.s.Err(); err != nil {
|
||||||
return err
|
return &DecoderError{"scan", d.curLine, err}
|
||||||
}
|
}
|
||||||
return d.parseErr
|
if d.parseErr != nil {
|
||||||
|
return &DecoderError{"parse", d.curLine, d.parseErr}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *MountInfoDecoder) scan() bool {
|
func (d *MountInfoDecoder) scan() bool {
|
||||||
@ -160,6 +190,7 @@ func (d *MountInfoDecoder) scan() bool {
|
|||||||
d.current.Next = m
|
d.current.Next = m
|
||||||
d.current = d.current.Next
|
d.current = d.current.Next
|
||||||
}
|
}
|
||||||
|
d.curLine++
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"iter"
|
"iter"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
@ -15,62 +16,102 @@ import (
|
|||||||
"hakurei.app/container/vfs"
|
"hakurei.app/container/vfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestDecoderError(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err *vfs.DecoderError
|
||||||
|
want string
|
||||||
|
target error
|
||||||
|
targetF error
|
||||||
|
}{
|
||||||
|
{"errno", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: syscall.ENOTRECOVERABLE},
|
||||||
|
"parse mountinfo at line 3735928559: state not recoverable", syscall.ENOTRECOVERABLE, syscall.EROFS},
|
||||||
|
|
||||||
|
{"strconv", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
|
||||||
|
`parse mountinfo at line 3735928559: numeric field "meow" invalid syntax`, strconv.ErrSyntax, os.ErrInvalid},
|
||||||
|
|
||||||
|
{"unfold", &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/proc/nonexistent")},
|
||||||
|
"unfold mountinfo: mount point /proc/nonexistent never appeared in mountinfo", vfs.UnfoldTargetError("/proc/nonexistent"), os.ErrNotExist},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Run("error", func(t *testing.T) {
|
||||||
|
if got := tc.err.Error(); got != tc.want {
|
||||||
|
t.Errorf("Error: %s, want %s", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("is", func(t *testing.T) {
|
||||||
|
if !errors.Is(tc.err, tc.target) {
|
||||||
|
t.Errorf("Is: unexpected false")
|
||||||
|
}
|
||||||
|
if errors.Is(tc.err, tc.targetF) {
|
||||||
|
t.Errorf("Is: unexpected true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMountInfo(t *testing.T) {
|
func TestMountInfo(t *testing.T) {
|
||||||
testCases := []mountInfoTest{
|
testCases := []mountInfoTest{
|
||||||
{"count", sampleMountinfoBase + `
|
{"count", sampleMountinfoBase + `
|
||||||
21 20 0:53/ /mnt/test rw,relatime - tmpfs rw
|
21 20 0:53/ /mnt/test rw,relatime - tmpfs rw
|
||||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
||||||
vfs.ErrMountInfoFields, "", nil, nil, nil},
|
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoFields},
|
||||||
|
"", nil, nil, nil},
|
||||||
|
|
||||||
{"sep", sampleMountinfoBase + `
|
{"sep", sampleMountinfoBase + `
|
||||||
21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw
|
21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw
|
||||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
||||||
vfs.ErrMountInfoSep, "", nil, nil, nil},
|
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoSep},
|
||||||
|
"", nil, nil, nil},
|
||||||
|
|
||||||
{"id", sampleMountinfoBase + `
|
{"id", sampleMountinfoBase + `
|
||||||
id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
|
id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
|
||||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
||||||
strconv.ErrSyntax, "", nil, nil, nil},
|
&vfs.DecoderError{Op: "parse", Line: 6, Err: &strconv.NumError{Func: "Atoi", Num: "id", Err: strconv.ErrSyntax}},
|
||||||
|
"", nil, nil, nil},
|
||||||
|
|
||||||
{"parent", sampleMountinfoBase + `
|
{"parent", sampleMountinfoBase + `
|
||||||
21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
|
21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
|
||||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
||||||
strconv.ErrSyntax, "", nil, nil, nil},
|
&vfs.DecoderError{Op: "parse", Line: 6, Err: &strconv.NumError{Func: "Atoi", Num: "parent", Err: strconv.ErrSyntax}}, "", nil, nil, nil},
|
||||||
|
|
||||||
{"devno", sampleMountinfoBase + `
|
{"devno", sampleMountinfoBase + `
|
||||||
21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw
|
21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw
|
||||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
||||||
nil, "unexpected EOF", nil, nil, nil},
|
nil, "parse mountinfo at line 6: unexpected EOF", nil, nil, nil},
|
||||||
|
|
||||||
{"maj", sampleMountinfoBase + `
|
{"maj", sampleMountinfoBase + `
|
||||||
21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
|
21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
|
||||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
||||||
nil, "expected integer", nil, nil, nil},
|
nil, "parse mountinfo at line 6: expected integer", nil, nil, nil},
|
||||||
|
|
||||||
{"min", sampleMountinfoBase + `
|
{"min", sampleMountinfoBase + `
|
||||||
21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw
|
21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw
|
||||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
||||||
nil, "expected integer", nil, nil, nil},
|
nil, "parse mountinfo at line 6: expected integer", nil, nil, nil},
|
||||||
|
|
||||||
{"mountroot", sampleMountinfoBase + `
|
{"mountroot", sampleMountinfoBase + `
|
||||||
21 20 0:53 /mnt/test rw,relatime - tmpfs rw
|
21 20 0:53 /mnt/test rw,relatime - tmpfs rw
|
||||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
||||||
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
|
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
|
||||||
|
|
||||||
{"target", sampleMountinfoBase + `
|
{"target", sampleMountinfoBase + `
|
||||||
21 20 0:53 / rw,relatime - tmpfs rw
|
21 20 0:53 / rw,relatime - tmpfs rw
|
||||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
||||||
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
|
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
|
||||||
|
|
||||||
{"vfs options", sampleMountinfoBase + `
|
{"vfs options", sampleMountinfoBase + `
|
||||||
21 20 0:53 / /mnt/test - tmpfs rw
|
21 20 0:53 / /mnt/test - tmpfs rw
|
||||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
||||||
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
|
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
|
||||||
|
|
||||||
{"FS type", sampleMountinfoBase + `
|
{"FS type", sampleMountinfoBase + `
|
||||||
21 20 0:53 / /mnt/test rw,relatime - rw
|
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
|
||||||
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
|
21 20 0:53 / /mnt/test rw,relatime - rw`,
|
||||||
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
|
&vfs.DecoderError{Op: "parse", Line: 7, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
|
||||||
|
|
||||||
{"base", sampleMountinfoBase, nil, "", []*wantMountInfo{
|
{"base", sampleMountinfoBase, nil, "", []*wantMountInfo{
|
||||||
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
|
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
|
||||||
@ -266,9 +307,9 @@ func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName s
|
|||||||
})
|
})
|
||||||
} else if tc.wantNode != nil || tc.wantCollectF != nil {
|
} else if tc.wantNode != nil || tc.wantCollectF != nil {
|
||||||
panic("invalid test case")
|
panic("invalid test case")
|
||||||
} else if _, err := d.Unfold("/"); !errors.Is(err, tc.wantErr) {
|
} else if _, err := d.Unfold("/"); !reflect.DeepEqual(err, tc.wantErr) {
|
||||||
if tc.wantError == "" {
|
if tc.wantError == "" {
|
||||||
t.Errorf("Unfold: error = %v, wantErr %v",
|
t.Errorf("Unfold: error = %#v, wantErr %#v",
|
||||||
err, tc.wantErr)
|
err, tc.wantErr)
|
||||||
} else if err != nil && err.Error() != tc.wantError {
|
} else if err != nil && err.Error() != tc.wantError {
|
||||||
t.Errorf("Unfold: error = %q, wantError %q",
|
t.Errorf("Unfold: error = %q, wantError %q",
|
||||||
@ -276,9 +317,9 @@ func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gotErr(); !errors.Is(err, tc.wantErr) {
|
if err := gotErr(); !reflect.DeepEqual(err, tc.wantErr) {
|
||||||
if tc.wantError == "" {
|
if tc.wantError == "" {
|
||||||
t.Errorf("%s: error = %v, wantErr %v",
|
t.Errorf("%s: error = %#v, wantErr %#v",
|
||||||
funcName, err, tc.wantErr)
|
funcName, err, tc.wantErr)
|
||||||
} else if err != nil && err.Error() != tc.wantError {
|
} else if err != nil && err.Error() != tc.wantError {
|
||||||
t.Errorf("%s: error = %q, wantError %q",
|
t.Errorf("%s: error = %q, wantError %q",
|
||||||
|
|||||||
@ -4,9 +4,14 @@ import (
|
|||||||
"iter"
|
"iter"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type UnfoldTargetError string
|
||||||
|
|
||||||
|
func (e UnfoldTargetError) Error() string {
|
||||||
|
return "mount point " + string(e) + " never appeared in mountinfo"
|
||||||
|
}
|
||||||
|
|
||||||
// MountInfoNode positions a [MountInfoEntry] in its mount hierarchy.
|
// MountInfoNode positions a [MountInfoEntry] in its mount hierarchy.
|
||||||
type MountInfoNode struct {
|
type MountInfoNode struct {
|
||||||
*MountInfoEntry
|
*MountInfoEntry
|
||||||
@ -65,7 +70,8 @@ func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if targetIndex == -1 {
|
if targetIndex == -1 {
|
||||||
return nil, syscall.ESTALE
|
// target does not exist in parsed mountinfo
|
||||||
|
return nil, &DecoderError{Op: "unfold", Line: -1, Err: UnfoldTargetError(targetClean)}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cur := range mountinfo {
|
for _, cur := range mountinfo {
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
package vfs_test
|
package vfs_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/container/vfs"
|
"hakurei.app/container/vfs"
|
||||||
@ -26,7 +24,7 @@ func TestUnfold(t *testing.T) {
|
|||||||
"no match",
|
"no match",
|
||||||
sampleMountinfoBase,
|
sampleMountinfoBase,
|
||||||
"/mnt",
|
"/mnt",
|
||||||
syscall.ESTALE, nil, nil, nil,
|
&vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/mnt")}, nil, nil, nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cover",
|
"cover",
|
||||||
@ -55,7 +53,7 @@ func TestUnfold(t *testing.T) {
|
|||||||
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
|
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
|
||||||
got, err := d.Unfold(tc.target)
|
got, err := d.Unfold(tc.target)
|
||||||
|
|
||||||
if !errors.Is(err, tc.wantErr) {
|
if !reflect.DeepEqual(err, tc.wantErr) {
|
||||||
t.Errorf("Unfold: error = %v, wantErr %v",
|
t.Errorf("Unfold: error = %v, wantErr %v",
|
||||||
err, tc.wantErr)
|
err, tc.wantErr)
|
||||||
}
|
}
|
||||||
|
|||||||
12
flake.lock
generated
12
flake.lock
generated
@ -7,11 +7,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1753479839,
|
"lastModified": 1756679287,
|
||||||
"narHash": "sha256-E/rPVh7vyPMJUFl2NAew+zibNGfVbANr8BP8nLRbLkQ=",
|
"narHash": "sha256-Xd1vOeY9ccDf5VtVK12yM0FS6qqvfUop8UQlxEB+gTQ=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "0b9bf983db4d064764084cd6748efb1ab8297d1e",
|
"rev": "07fc025fe10487dd80f2ec694f1cd790e752d0e8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -23,11 +23,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1753345091,
|
"lastModified": 1757020766,
|
||||||
"narHash": "sha256-CdX2Rtvp5I8HGu9swBmYuq+ILwRxpXdJwlpg8jvN4tU=",
|
"narHash": "sha256-PLoSjHRa2bUbi1x9HoXgTx2AiuzNXs54c8omhadyvp0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "3ff0e34b1383648053bba8ed03f201d3466f90c9",
|
"rev": "fe83bbdde2ccdc2cb9573aa846abe8363f79a97a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@ -11,10 +11,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestContainer(t *testing.T) {
|
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)
|
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 {
|
if err := h.Start(); err == nil || err.Error() != wantErr {
|
||||||
t.Errorf("Start: error = %v, wantErr %q",
|
t.Errorf("Start: error = %v, wantErr %q",
|
||||||
err, wantErr)
|
err, wantErr)
|
||||||
|
|||||||
@ -6,8 +6,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -47,6 +49,10 @@ func argFChecked(argsFd, statFd int) (args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
containerTimeout = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
// this function tests an implementation of the helper.Helper interface
|
// this function tests an implementation of the helper.Helper interface
|
||||||
func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper) {
|
func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper) {
|
||||||
oldWaitDelay := helper.WaitDelay
|
oldWaitDelay := helper.WaitDelay
|
||||||
@ -54,18 +60,15 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
|
|||||||
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
|
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
|
||||||
|
|
||||||
t.Run("start helper with status channel and wait", func(t *testing.T) {
|
t.Run("start helper with status channel and wait", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(t.Context(), containerTimeout)
|
||||||
stdout := new(strings.Builder)
|
stdout := new(strings.Builder)
|
||||||
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, true)
|
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, true)
|
||||||
|
|
||||||
t.Run("wait not yet started helper", func(t *testing.T) {
|
t.Run("wait not yet started helper", func(t *testing.T) {
|
||||||
defer func() {
|
if err := h.Wait(); !reflect.DeepEqual(err, syscall.EINVAL) &&
|
||||||
r := recover()
|
!reflect.DeepEqual(err, errors.New("exec: not started")) {
|
||||||
if r == nil {
|
t.Errorf("Wait: error = %v", err)
|
||||||
t.Fatalf("Wait did not panic")
|
}
|
||||||
}
|
|
||||||
}()
|
|
||||||
panic(fmt.Sprintf("unreachable: %v", h.Wait()))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Log("starting helper stub")
|
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) {
|
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()
|
defer cancel()
|
||||||
stdout := new(strings.Builder)
|
stdout := new(strings.Builder)
|
||||||
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false)
|
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false)
|
||||||
|
|||||||
@ -87,7 +87,9 @@ type (
|
|||||||
|
|
||||||
// initial process environment variables
|
// initial process environment variables
|
||||||
Env map[string]string `json:"env"`
|
Env map[string]string `json:"env"`
|
||||||
// map target user uid to privileged user uid in the user namespace
|
// map target user uid to privileged user uid in the user namespace;
|
||||||
|
// some programs fail to connect to dbus session running as a different uid,
|
||||||
|
// this option works around it by mapping priv-side caller uid in container
|
||||||
MapRealUID bool `json:"map_real_uid"`
|
MapRealUID bool `json:"map_real_uid"`
|
||||||
|
|
||||||
// pass through all devices
|
// pass through all devices
|
||||||
|
|||||||
30
hst/hst.go
30
hst/hst.go
@ -2,12 +2,42 @@
|
|||||||
package hst
|
package hst
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
"hakurei.app/container/seccomp"
|
"hakurei.app/container/seccomp"
|
||||||
"hakurei.app/system"
|
"hakurei.app/system"
|
||||||
"hakurei.app/system/dbus"
|
"hakurei.app/system/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// An AppError is returned while starting an app according to [hst.Config].
|
||||||
|
type AppError struct {
|
||||||
|
Step string
|
||||||
|
Err error
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AppError) Error() string { return e.Err.Error() }
|
||||||
|
func (e *AppError) Unwrap() error { return e.Err }
|
||||||
|
func (e *AppError) Message() string {
|
||||||
|
if e.Msg != "" {
|
||||||
|
return e.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case errors.As(e.Err, new(*os.PathError)),
|
||||||
|
errors.As(e.Err, new(*os.LinkError)),
|
||||||
|
errors.As(e.Err, new(*os.SyscallError)),
|
||||||
|
errors.As(e.Err, new(*net.OpError)):
|
||||||
|
return "cannot " + e.Error()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "cannot " + e.Step + ": " + e.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Paths contains environment-dependent paths used by hakurei.
|
// Paths contains environment-dependent paths used by hakurei.
|
||||||
type Paths struct {
|
type Paths struct {
|
||||||
// temporary directory returned by [os.TempDir] (usually `/tmp`)
|
// temporary directory returned by [os.TempDir] (usually `/tmp`)
|
||||||
|
|||||||
@ -2,11 +2,93 @@ package hst_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/container/stub"
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestAppError(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
s string
|
||||||
|
message string
|
||||||
|
is, isF error
|
||||||
|
}{
|
||||||
|
{"message", &hst.AppError{Step: "obtain uid from hsu", Err: stub.UniqueError(0),
|
||||||
|
Msg: "the setuid helper is missing: /run/wrappers/bin/hsu"},
|
||||||
|
"unique error 0 injected by the test suite",
|
||||||
|
"the setuid helper is missing: /run/wrappers/bin/hsu",
|
||||||
|
stub.UniqueError(0), os.ErrNotExist},
|
||||||
|
|
||||||
|
{"os.PathError", &hst.AppError{Step: "passthrough os.PathError",
|
||||||
|
Err: &os.PathError{Op: "stat", Path: "/proc/nonexistent", Err: os.ErrNotExist}},
|
||||||
|
"stat /proc/nonexistent: file does not exist",
|
||||||
|
"cannot stat /proc/nonexistent: file does not exist",
|
||||||
|
os.ErrNotExist, stub.UniqueError(0xdeadbeef)},
|
||||||
|
|
||||||
|
{"os.LinkError", &hst.AppError{Step: "passthrough os.LinkError",
|
||||||
|
Err: &os.LinkError{Op: "link", Old: "/proc/self", New: "/proc/nonexistent", Err: os.ErrNotExist}},
|
||||||
|
"link /proc/self /proc/nonexistent: file does not exist",
|
||||||
|
"cannot link /proc/self /proc/nonexistent: file does not exist",
|
||||||
|
os.ErrNotExist, stub.UniqueError(0xdeadbeef)},
|
||||||
|
|
||||||
|
{"os.SyscallError", &hst.AppError{Step: "passthrough os.SyscallError",
|
||||||
|
Err: &os.SyscallError{Syscall: "meow", Err: syscall.ENOSYS}},
|
||||||
|
"meow: function not implemented",
|
||||||
|
"cannot meow: function not implemented",
|
||||||
|
syscall.ENOSYS, syscall.ENOTRECOVERABLE},
|
||||||
|
|
||||||
|
{"net.OpError", &hst.AppError{Step: "passthrough net.OpError",
|
||||||
|
Err: &net.OpError{Op: "dial", Net: "cat", Err: net.UnknownNetworkError("cat")}},
|
||||||
|
"dial cat: unknown network cat",
|
||||||
|
"cannot dial cat: unknown network cat",
|
||||||
|
net.UnknownNetworkError("cat"), syscall.ENOTRECOVERABLE},
|
||||||
|
|
||||||
|
{"default", &hst.AppError{Step: "initialise container configuration", Err: stub.UniqueError(1)},
|
||||||
|
"unique error 1 injected by the test suite",
|
||||||
|
"cannot initialise container configuration: unique error 1 injected by the test suite",
|
||||||
|
stub.UniqueError(1), os.ErrInvalid},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Run("error", func(t *testing.T) {
|
||||||
|
if got := tc.err.Error(); got != tc.s {
|
||||||
|
t.Errorf("Error: %s, want %s", got, tc.s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("message", func(t *testing.T) {
|
||||||
|
gotMessage, gotMessageOk := container.GetErrorMessage(tc.err)
|
||||||
|
if want := tc.message != "\x00"; gotMessageOk != want {
|
||||||
|
t.Errorf("GetErrorMessage: ok = %v, want %v", gotMessage, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotMessageOk {
|
||||||
|
if gotMessage != tc.message {
|
||||||
|
t.Errorf("GetErrorMessage: %s, want %s", gotMessage, tc.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("is", func(t *testing.T) {
|
||||||
|
if !errors.Is(tc.err, tc.is) {
|
||||||
|
t.Errorf("Is: unexpected false for %v", tc.is)
|
||||||
|
}
|
||||||
|
if errors.Is(tc.err, tc.isF) {
|
||||||
|
t.Errorf("Is: unexpected true for %v", tc.isF)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTemplate(t *testing.T) {
|
func TestTemplate(t *testing.T) {
|
||||||
const want = `{
|
const want = `{
|
||||||
"id": "org.chromium.Chromium",
|
"id": "org.chromium.Chromium",
|
||||||
|
|||||||
@ -1,61 +1,28 @@
|
|||||||
// Package app defines the generic [App] interface.
|
// Package app implements high-level hakurei container behaviour.
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"syscall"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/internal/app/state"
|
"hakurei.app/internal/app/state"
|
||||||
"hakurei.app/internal/sys"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type App interface {
|
// Main runs an app according to [hst.Config] and terminates. Main does not return.
|
||||||
// ID returns a copy of [ID] held by App.
|
func Main(ctx context.Context, config *hst.Config) {
|
||||||
ID() state.ID
|
var id state.ID
|
||||||
|
if err := state.NewAppID(&id); err != nil {
|
||||||
// Seal determines the outcome of config as a [SealedApp].
|
log.Fatal(err)
|
||||||
// The value of config might be overwritten and must not be used again.
|
|
||||||
Seal(config *hst.Config) (SealedApp, error)
|
|
||||||
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SealedApp interface {
|
|
||||||
// Run commits sealed system setup and starts the app process.
|
|
||||||
Run(rs *RunState) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunState stores the outcome of a call to [SealedApp.Run].
|
|
||||||
type RunState struct {
|
|
||||||
// Time is the exact point in time where the process was created.
|
|
||||||
// Location must be set to UTC.
|
|
||||||
//
|
|
||||||
// Time is nil if no process was ever created.
|
|
||||||
Time *time.Time
|
|
||||||
// RevertErr is stored by the deferred revert call.
|
|
||||||
RevertErr error
|
|
||||||
// WaitErr is the generic error value created by the standard library.
|
|
||||||
WaitErr error
|
|
||||||
|
|
||||||
syscall.WaitStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetStart stores the current time in [RunState] once.
|
|
||||||
func (rs *RunState) SetStart() {
|
|
||||||
if rs.Time != nil {
|
|
||||||
panic("attempted to store time twice")
|
|
||||||
}
|
}
|
||||||
now := time.Now().UTC()
|
|
||||||
rs.Time = &now
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustNew(ctx context.Context, os sys.State) App {
|
seal := outcome{id: &stringPair[state.ID]{id, id.String()}, syscallDispatcher: direct{}}
|
||||||
a, err := New(ctx, os)
|
if err := seal.finalise(ctx, config); err != nil {
|
||||||
if err != nil {
|
printMessageError("cannot seal app:", err)
|
||||||
log.Fatalf("cannot create app: %v", err)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return a
|
|
||||||
|
seal.main()
|
||||||
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,69 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"hakurei.app/hst"
|
|
||||||
"hakurei.app/internal/app/state"
|
|
||||||
"hakurei.app/internal/sys"
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(ctx context.Context, os sys.State) (App, error) {
|
|
||||||
a := new(app)
|
|
||||||
a.sys = os
|
|
||||||
a.ctx = ctx
|
|
||||||
|
|
||||||
id := new(state.ID)
|
|
||||||
err := state.NewAppID(id)
|
|
||||||
a.id = newID(id)
|
|
||||||
|
|
||||||
return a, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type app struct {
|
|
||||||
id *stringPair[state.ID]
|
|
||||||
sys sys.State
|
|
||||||
ctx context.Context
|
|
||||||
|
|
||||||
*outcome
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *app) ID() state.ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
|
|
||||||
|
|
||||||
func (a *app) String() string {
|
|
||||||
if a == nil {
|
|
||||||
return "(invalid app)"
|
|
||||||
}
|
|
||||||
|
|
||||||
a.mu.RLock()
|
|
||||||
defer a.mu.RUnlock()
|
|
||||||
|
|
||||||
if a.outcome != nil {
|
|
||||||
if a.outcome.user.uid == nil {
|
|
||||||
return fmt.Sprintf("(sealed app %s with invalid uid)", a.id)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("(sealed app %s as uid %s)", a.id, a.outcome.user.uid)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("(unsealed app %s)", a.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *app) Seal(config *hst.Config) (SealedApp, error) {
|
|
||||||
a.mu.Lock()
|
|
||||||
defer a.mu.Unlock()
|
|
||||||
|
|
||||||
if a.outcome != nil {
|
|
||||||
panic("app sealed twice")
|
|
||||||
}
|
|
||||||
|
|
||||||
seal := new(outcome)
|
|
||||||
seal.id = a.id
|
|
||||||
err := seal.finalise(a.ctx, a.sys, config)
|
|
||||||
if err == nil {
|
|
||||||
a.outcome = seal
|
|
||||||
}
|
|
||||||
return seal, err
|
|
||||||
}
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
package app_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io/fs"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"hakurei.app/container"
|
|
||||||
"hakurei.app/hst"
|
|
||||||
"hakurei.app/internal/app"
|
|
||||||
"hakurei.app/internal/app/state"
|
|
||||||
"hakurei.app/internal/hlog"
|
|
||||||
"hakurei.app/internal/sys"
|
|
||||||
"hakurei.app/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
type sealTestCase struct {
|
|
||||||
name string
|
|
||||||
os sys.State
|
|
||||||
config *hst.Config
|
|
||||||
id state.ID
|
|
||||||
wantSys *system.I
|
|
||||||
wantContainer *container.Params
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApp(t *testing.T) {
|
|
||||||
testCases := append(testCasesPd, testCasesNixos...)
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
a := app.NewWithID(tc.id, tc.os)
|
|
||||||
var (
|
|
||||||
gotSys *system.I
|
|
||||||
gotContainer *container.Params
|
|
||||||
)
|
|
||||||
if !t.Run("seal", func(t *testing.T) {
|
|
||||||
if sa, err := a.Seal(tc.config); err != nil {
|
|
||||||
hlog.PrintBaseError(err, "got generic error:")
|
|
||||||
t.Errorf("Seal: error = %v", err)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
gotSys, gotContainer = app.AppIParams(a, sa)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("compare sys", func(t *testing.T) {
|
|
||||||
if !gotSys.Equal(tc.wantSys) {
|
|
||||||
t.Errorf("Seal: sys = %#v, want %#v",
|
|
||||||
gotSys, tc.wantSys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("compare params", func(t *testing.T) {
|
|
||||||
if !reflect.DeepEqual(gotContainer, tc.wantContainer) {
|
|
||||||
t.Errorf("seal: params =\n%s\n, want\n%s",
|
|
||||||
mustMarshal(gotContainer), mustMarshal(tc.wantContainer))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustMarshal(v any) string {
|
|
||||||
if b, err := json.Marshal(v); err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
} else {
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stubDirEntries(names ...string) (e []fs.DirEntry, err error) {
|
|
||||||
e = make([]fs.DirEntry, len(names))
|
|
||||||
for i, name := range names {
|
|
||||||
e[i] = stubDirEntryPath(name)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type stubDirEntryPath string
|
|
||||||
|
|
||||||
func (p stubDirEntryPath) Name() string { return string(p) }
|
|
||||||
func (p stubDirEntryPath) IsDir() bool { panic("attempted to call IsDir") }
|
|
||||||
func (p stubDirEntryPath) Type() fs.FileMode { panic("attempted to call Type") }
|
|
||||||
func (p stubDirEntryPath) Info() (fs.FileInfo, error) { panic("attempted to call Info") }
|
|
||||||
|
|
||||||
type stubFileInfoMode fs.FileMode
|
|
||||||
|
|
||||||
func (s stubFileInfoMode) Name() string { panic("attempted to call Name") }
|
|
||||||
func (s stubFileInfoMode) Size() int64 { panic("attempted to call Size") }
|
|
||||||
func (s stubFileInfoMode) Mode() fs.FileMode { return fs.FileMode(s) }
|
|
||||||
func (s stubFileInfoMode) ModTime() time.Time { panic("attempted to call ModTime") }
|
|
||||||
func (s stubFileInfoMode) IsDir() bool { panic("attempted to call IsDir") }
|
|
||||||
func (s stubFileInfoMode) Sys() any { panic("attempted to call Sys") }
|
|
||||||
|
|
||||||
type stubFileInfoIsDir bool
|
|
||||||
|
|
||||||
func (s stubFileInfoIsDir) Name() string { panic("attempted to call Name") }
|
|
||||||
func (s stubFileInfoIsDir) Size() int64 { panic("attempted to call Size") }
|
|
||||||
func (s stubFileInfoIsDir) Mode() fs.FileMode { panic("attempted to call Mode") }
|
|
||||||
func (s stubFileInfoIsDir) ModTime() time.Time { panic("attempted to call ModTime") }
|
|
||||||
func (s stubFileInfoIsDir) IsDir() bool { return bool(s) }
|
|
||||||
func (s stubFileInfoIsDir) Sys() any { panic("attempted to call Sys") }
|
|
||||||
@ -1,168 +0,0 @@
|
|||||||
package app_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"hakurei.app/container"
|
|
||||||
"hakurei.app/container/seccomp"
|
|
||||||
"hakurei.app/hst"
|
|
||||||
"hakurei.app/internal/app/state"
|
|
||||||
"hakurei.app/system"
|
|
||||||
"hakurei.app/system/acl"
|
|
||||||
"hakurei.app/system/dbus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func m(pathname string) *container.Absolute { return container.MustAbs(pathname) }
|
|
||||||
func f(c hst.FilesystemConfig) hst.FilesystemConfigJSON {
|
|
||||||
return hst.FilesystemConfigJSON{FilesystemConfig: c}
|
|
||||||
}
|
|
||||||
|
|
||||||
var testCasesNixos = []sealTestCase{
|
|
||||||
{
|
|
||||||
"nixos chromium direct wayland", new(stubNixOS),
|
|
||||||
&hst.Config{
|
|
||||||
ID: "org.chromium.Chromium",
|
|
||||||
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
|
|
||||||
Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse),
|
|
||||||
Shell: m("/run/current-system/sw/bin/zsh"),
|
|
||||||
|
|
||||||
Container: &hst.ContainerConfig{
|
|
||||||
Userns: true, HostNet: true, MapRealUID: true, Env: nil,
|
|
||||||
Filesystem: []hst.FilesystemConfigJSON{
|
|
||||||
f(&hst.FSBind{Source: m("/bin")}),
|
|
||||||
f(&hst.FSBind{Source: m("/usr/bin/")}),
|
|
||||||
f(&hst.FSBind{Source: m("/nix/store")}),
|
|
||||||
f(&hst.FSBind{Source: m("/run/current-system")}),
|
|
||||||
f(&hst.FSBind{Source: m("/sys/block"), Optional: true}),
|
|
||||||
f(&hst.FSBind{Source: m("/sys/bus"), Optional: true}),
|
|
||||||
f(&hst.FSBind{Source: m("/sys/class"), Optional: true}),
|
|
||||||
f(&hst.FSBind{Source: m("/sys/dev"), Optional: true}),
|
|
||||||
f(&hst.FSBind{Source: m("/sys/devices"), Optional: true}),
|
|
||||||
f(&hst.FSBind{Source: m("/run/opengl-driver")}),
|
|
||||||
f(&hst.FSBind{Source: m("/dev/dri"), Device: true, Optional: true}),
|
|
||||||
f(&hst.FSBind{Source: m("/etc/"), Target: m("/etc/"), Special: true}),
|
|
||||||
f(&hst.FSBind{Source: m("/var/lib/persist/module/hakurei/0/1"), Write: true, Ensure: true}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
SystemBus: &dbus.Config{
|
|
||||||
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
|
|
||||||
Filter: true,
|
|
||||||
},
|
|
||||||
SessionBus: &dbus.Config{
|
|
||||||
Talk: []string{
|
|
||||||
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
|
|
||||||
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
|
|
||||||
"org.kde.kwalletd5", "org.kde.kwalletd6",
|
|
||||||
},
|
|
||||||
Own: []string{
|
|
||||||
"org.chromium.Chromium.*",
|
|
||||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
|
||||||
"org.mpris.MediaPlayer2.chromium.*",
|
|
||||||
},
|
|
||||||
Call: map[string]string{}, Broadcast: map[string]string{},
|
|
||||||
Filter: true,
|
|
||||||
},
|
|
||||||
DirectWayland: true,
|
|
||||||
|
|
||||||
Username: "u0_a1",
|
|
||||||
Home: m("/var/lib/persist/module/hakurei/0/1"),
|
|
||||||
Identity: 1, Groups: []string{},
|
|
||||||
},
|
|
||||||
state.ID{
|
|
||||||
0x8e, 0x2c, 0x76, 0xb0,
|
|
||||||
0x66, 0xda, 0xbe, 0x57,
|
|
||||||
0x4c, 0xf0, 0x73, 0xbd,
|
|
||||||
0xb4, 0x6e, 0xb5, 0xc1,
|
|
||||||
},
|
|
||||||
system.New(1000001).
|
|
||||||
Ensure("/tmp/hakurei.1971", 0711).
|
|
||||||
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
|
|
||||||
Ensure("/tmp/hakurei.1971/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/1", acl.Read, acl.Write, acl.Execute).
|
|
||||||
Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute).
|
|
||||||
Ensure("/tmp/hakurei.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute).
|
|
||||||
Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute).
|
|
||||||
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
|
||||||
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
|
|
||||||
Ephemeral(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
|
|
||||||
Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
|
|
||||||
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
|
|
||||||
Ephemeral(system.Process, "/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
|
|
||||||
MustProxyDBus("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
|
|
||||||
Talk: []string{
|
|
||||||
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
|
|
||||||
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
|
|
||||||
"org.kde.kwalletd5", "org.kde.kwalletd6",
|
|
||||||
},
|
|
||||||
Own: []string{
|
|
||||||
"org.chromium.Chromium.*",
|
|
||||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
|
||||||
"org.mpris.MediaPlayer2.chromium.*",
|
|
||||||
},
|
|
||||||
Call: map[string]string{}, Broadcast: map[string]string{},
|
|
||||||
Filter: true,
|
|
||||||
}, "/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
|
|
||||||
Talk: []string{
|
|
||||||
"org.bluez",
|
|
||||||
"org.freedesktop.Avahi",
|
|
||||||
"org.freedesktop.UPower",
|
|
||||||
},
|
|
||||||
Filter: true,
|
|
||||||
}).
|
|
||||||
UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
|
|
||||||
UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
|
|
||||||
&container.Params{
|
|
||||||
Uid: 1971,
|
|
||||||
Gid: 100,
|
|
||||||
Dir: m("/var/lib/persist/module/hakurei/0/1"),
|
|
||||||
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
|
|
||||||
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
|
|
||||||
Env: []string{
|
|
||||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
|
|
||||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
|
|
||||||
"HOME=/var/lib/persist/module/hakurei/0/1",
|
|
||||||
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
|
|
||||||
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
|
|
||||||
"SHELL=/run/current-system/sw/bin/zsh",
|
|
||||||
"TERM=xterm-256color",
|
|
||||||
"USER=u0_a1",
|
|
||||||
"WAYLAND_DISPLAY=wayland-0",
|
|
||||||
"XDG_RUNTIME_DIR=/run/user/1971",
|
|
||||||
"XDG_SESSION_CLASS=user",
|
|
||||||
"XDG_SESSION_TYPE=tty",
|
|
||||||
},
|
|
||||||
Ops: new(container.Ops).
|
|
||||||
Proc(m("/proc/")).
|
|
||||||
Tmpfs(hst.AbsTmp, 4096, 0755).
|
|
||||||
DevWritable(m("/dev/"), true).
|
|
||||||
Bind(m("/bin"), m("/bin"), 0).
|
|
||||||
Bind(m("/usr/bin/"), m("/usr/bin/"), 0).
|
|
||||||
Bind(m("/nix/store"), m("/nix/store"), 0).
|
|
||||||
Bind(m("/run/current-system"), m("/run/current-system"), 0).
|
|
||||||
Bind(m("/sys/block"), m("/sys/block"), container.BindOptional).
|
|
||||||
Bind(m("/sys/bus"), m("/sys/bus"), container.BindOptional).
|
|
||||||
Bind(m("/sys/class"), m("/sys/class"), container.BindOptional).
|
|
||||||
Bind(m("/sys/dev"), m("/sys/dev"), container.BindOptional).
|
|
||||||
Bind(m("/sys/devices"), m("/sys/devices"), container.BindOptional).
|
|
||||||
Bind(m("/run/opengl-driver"), m("/run/opengl-driver"), 0).
|
|
||||||
Bind(m("/dev/dri"), m("/dev/dri"), container.BindDevice|container.BindWritable|container.BindOptional).
|
|
||||||
Etc(m("/etc/"), "8e2c76b066dabe574cf073bdb46eb5c1").
|
|
||||||
Bind(m("/var/lib/persist/module/hakurei/0/1"), m("/var/lib/persist/module/hakurei/0/1"), container.BindWritable|container.BindEnsure).
|
|
||||||
Remount(m("/dev/"), syscall.MS_RDONLY).
|
|
||||||
Tmpfs(m("/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,212 +0,0 @@
|
|||||||
package app_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"hakurei.app/container"
|
|
||||||
"hakurei.app/container/seccomp"
|
|
||||||
"hakurei.app/hst"
|
|
||||||
"hakurei.app/internal/app/state"
|
|
||||||
"hakurei.app/system"
|
|
||||||
"hakurei.app/system/acl"
|
|
||||||
"hakurei.app/system/dbus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var testCasesPd = []sealTestCase{
|
|
||||||
{
|
|
||||||
"nixos permissive defaults no enablements", new(stubNixOS),
|
|
||||||
&hst.Config{Username: "chronos", Home: m("/home/chronos")},
|
|
||||||
state.ID{
|
|
||||||
0x4a, 0x45, 0x0b, 0x65,
|
|
||||||
0x96, 0xd7, 0xbc, 0x15,
|
|
||||||
0xbd, 0x01, 0x78, 0x0e,
|
|
||||||
0xb9, 0xa6, 0x07, 0xac,
|
|
||||||
},
|
|
||||||
system.New(1000000).
|
|
||||||
Ensure("/tmp/hakurei.1971", 0711).
|
|
||||||
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
|
|
||||||
Ensure("/tmp/hakurei.1971/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/0", acl.Read, acl.Write, acl.Execute).
|
|
||||||
Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute).
|
|
||||||
Ensure("/tmp/hakurei.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
|
|
||||||
&container.Params{
|
|
||||||
Dir: m("/home/chronos"),
|
|
||||||
Path: m("/run/current-system/sw/bin/zsh"),
|
|
||||||
Args: []string{"/run/current-system/sw/bin/zsh"},
|
|
||||||
Env: []string{
|
|
||||||
"HOME=/home/chronos",
|
|
||||||
"SHELL=/run/current-system/sw/bin/zsh",
|
|
||||||
"TERM=xterm-256color",
|
|
||||||
"USER=chronos",
|
|
||||||
"XDG_RUNTIME_DIR=/run/user/65534",
|
|
||||||
"XDG_SESSION_CLASS=user",
|
|
||||||
"XDG_SESSION_TYPE=tty",
|
|
||||||
},
|
|
||||||
Ops: new(container.Ops).
|
|
||||||
Root(m("/"), container.BindWritable).
|
|
||||||
Proc(m("/proc/")).
|
|
||||||
Tmpfs(hst.AbsTmp, 4096, 0755).
|
|
||||||
DevWritable(m("/dev/"), true).
|
|
||||||
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
|
|
||||||
Readonly(m("/var/run/nscd"), 0755).
|
|
||||||
Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac").
|
|
||||||
Tmpfs(m("/run/user/1971"), 8192, 0755).
|
|
||||||
Tmpfs(m("/run/dbus"), 8192, 0755).
|
|
||||||
Remount(m("/dev/"), syscall.MS_RDONLY).
|
|
||||||
Tmpfs(m("/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(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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"hakurei.app/hst"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// fs methods are not implemented using a real FS
|
|
||||||
// to help better understand filesystem access behaviour
|
|
||||||
type stubNixOS struct {
|
type stubNixOS struct {
|
||||||
lookPathErr map[string]error
|
lookPathErr map[string]error
|
||||||
usernameErr map[string]error
|
usernameErr map[string]error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stubNixOS) Getuid() int { return 1971 }
|
func (k *stubNixOS) new(func(k syscallDispatcher)) { panic("not implemented") }
|
||||||
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 (s *stubNixOS) Println(v ...any) { log.Println(v...) }
|
func (k *stubNixOS) getuid() int { return 1971 }
|
||||||
func (s *stubNixOS) Printf(format string, v ...any) { log.Printf(format, v...) }
|
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 {
|
switch key {
|
||||||
case "SHELL":
|
case "SHELL":
|
||||||
return "/run/current-system/sw/bin/zsh", true
|
return "/run/current-system/sw/bin/zsh", true
|
||||||
@ -40,6 +30,8 @@ func (s *stubNixOS) LookupEnv(key string) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
case "HOME":
|
case "HOME":
|
||||||
return "/home/ophestra", true
|
return "/home/ophestra", true
|
||||||
|
case "XDG_RUNTIME_DIR":
|
||||||
|
return "/run/user/1971", true
|
||||||
case "XDG_CONFIG_HOME":
|
case "XDG_CONFIG_HOME":
|
||||||
return "/home/ophestra/xdg/config", true
|
return "/home/ophestra/xdg/config", true
|
||||||
default:
|
default:
|
||||||
@ -47,61 +39,7 @@ func (s *stubNixOS) LookupEnv(key string) (string, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stubNixOS) LookPath(file string) (string, error) {
|
func (k *stubNixOS) stat(name string) (fs.FileInfo, error) {
|
||||||
if s.lookPathErr != nil {
|
|
||||||
if err, ok := s.lookPathErr[file]; ok {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch file {
|
|
||||||
case "zsh":
|
|
||||||
return "/run/current-system/sw/bin/zsh", nil
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubNixOS) LookupGroup(name string) (*user.Group, error) {
|
|
||||||
switch name {
|
|
||||||
case "video":
|
|
||||||
return &user.Group{Gid: "26", Name: "video"}, nil
|
|
||||||
default:
|
|
||||||
return nil, user.UnknownGroupError(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubNixOS) ReadDir(name string) ([]fs.DirEntry, error) {
|
|
||||||
switch name {
|
|
||||||
case "/":
|
|
||||||
return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib",
|
|
||||||
"lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
|
|
||||||
case "/run":
|
|
||||||
return stubDirEntries("agetty.reload", "binfmt", "booted-system",
|
|
||||||
"credentials", "cryptsetup", "current-system", "dbus", "host", "keys",
|
|
||||||
"libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager",
|
|
||||||
"nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm",
|
|
||||||
"store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2",
|
|
||||||
"user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state")
|
|
||||||
case "/etc":
|
|
||||||
return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default",
|
|
||||||
"ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid",
|
|
||||||
"hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd",
|
|
||||||
"libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm",
|
|
||||||
"machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc",
|
|
||||||
"netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf",
|
|
||||||
"opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1",
|
|
||||||
"profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba",
|
|
||||||
"sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh",
|
|
||||||
"ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo",
|
|
||||||
"tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc",
|
|
||||||
"zoneinfo", "zprofile", "zshenv", "zshrc")
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("attempted to read unexpected directory %q", name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubNixOS) Stat(name string) (fs.FileInfo, error) {
|
|
||||||
switch name {
|
switch name {
|
||||||
case "/var/run/nscd":
|
case "/var/run/nscd":
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -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 {
|
switch name {
|
||||||
|
case "/":
|
||||||
|
return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib",
|
||||||
|
"lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
|
||||||
|
|
||||||
|
case "/run":
|
||||||
|
return stubDirEntries("agetty.reload", "binfmt", "booted-system",
|
||||||
|
"credentials", "cryptsetup", "current-system", "dbus", "host", "keys",
|
||||||
|
"libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager",
|
||||||
|
"nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm",
|
||||||
|
"store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2",
|
||||||
|
"user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state")
|
||||||
|
|
||||||
|
case "/etc":
|
||||||
|
return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default",
|
||||||
|
"ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid",
|
||||||
|
"hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd",
|
||||||
|
"libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm",
|
||||||
|
"machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc",
|
||||||
|
"netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf",
|
||||||
|
"opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1",
|
||||||
|
"profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba",
|
||||||
|
"sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh",
|
||||||
|
"ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo",
|
||||||
|
"tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc",
|
||||||
|
"zoneinfo", "zprofile", "zshenv", "zshrc")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("attempted to open unexpected file %q", name))
|
panic(fmt.Sprintf("attempted to read unexpected directory %q", name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stubNixOS) Paths() hst.Paths {
|
func (k *stubNixOS) tempdir() string { return "/tmp/" }
|
||||||
return hst.Paths{
|
|
||||||
SharePath: m("/tmp/hakurei.1971"),
|
func (k *stubNixOS) evalSymlinks(path string) (string, error) {
|
||||||
RuntimePath: m("/run/user/1971"),
|
switch path {
|
||||||
RunDirPath: m("/run/user/1971/hakurei"),
|
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...) }
|
||||||
452
internal/app/app_test.go
Normal file
452
internal/app/app_test.go
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/container/seccomp"
|
||||||
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/internal/app/state"
|
||||||
|
"hakurei.app/system"
|
||||||
|
"hakurei.app/system/acl"
|
||||||
|
"hakurei.app/system/dbus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApp(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
k syscallDispatcher
|
||||||
|
config *hst.Config
|
||||||
|
id state.ID
|
||||||
|
wantSys *system.I
|
||||||
|
wantParams *container.Params
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"nixos permissive defaults no enablements", new(stubNixOS),
|
||||||
|
&hst.Config{Username: "chronos", Home: m("/home/chronos")},
|
||||||
|
state.ID{
|
||||||
|
0x4a, 0x45, 0x0b, 0x65,
|
||||||
|
0x96, 0xd7, 0xbc, 0x15,
|
||||||
|
0xbd, 0x01, 0x78, 0x0e,
|
||||||
|
0xb9, 0xa6, 0x07, 0xac,
|
||||||
|
},
|
||||||
|
system.New(context.TODO(), 1000000).
|
||||||
|
Ensure("/tmp/hakurei.0", 0711).
|
||||||
|
Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute).
|
||||||
|
Ensure("/tmp/hakurei.0/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/0", acl.Read, acl.Write, acl.Execute).
|
||||||
|
Ensure("/tmp/hakurei.0/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir", acl.Execute).
|
||||||
|
Ensure("/tmp/hakurei.0/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir/0", acl.Read, acl.Write, acl.Execute),
|
||||||
|
&container.Params{
|
||||||
|
Dir: m("/home/chronos"),
|
||||||
|
Path: m("/run/current-system/sw/bin/zsh"),
|
||||||
|
Args: []string{"/run/current-system/sw/bin/zsh"},
|
||||||
|
Env: []string{
|
||||||
|
"HOME=/home/chronos",
|
||||||
|
"SHELL=/run/current-system/sw/bin/zsh",
|
||||||
|
"TERM=xterm-256color",
|
||||||
|
"USER=chronos",
|
||||||
|
"XDG_RUNTIME_DIR=/run/user/65534",
|
||||||
|
"XDG_SESSION_CLASS=user",
|
||||||
|
"XDG_SESSION_TYPE=tty",
|
||||||
|
},
|
||||||
|
Ops: new(container.Ops).
|
||||||
|
Root(m("/"), container.BindWritable).
|
||||||
|
Proc(m("/proc/")).
|
||||||
|
Tmpfs(hst.AbsTmp, 4096, 0755).
|
||||||
|
DevWritable(m("/dev/"), true).
|
||||||
|
Tmpfs(m("/dev/shm"), 0, 01777).
|
||||||
|
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
|
||||||
|
Readonly(m("/var/run/nscd"), 0755).
|
||||||
|
Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac").
|
||||||
|
Tmpfs(m("/run/user/1971"), 8192, 0755).
|
||||||
|
Tmpfs(m("/run/dbus"), 8192, 0755).
|
||||||
|
Remount(m("/dev/"), syscall.MS_RDONLY).
|
||||||
|
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||||
|
Bind(m("/tmp/hakurei.0/runtime/0"), m("/run/user/65534"), container.BindWritable).
|
||||||
|
Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), container.BindWritable).
|
||||||
|
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
||||||
|
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
|
||||||
|
Remount(m("/"), syscall.MS_RDONLY),
|
||||||
|
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
|
||||||
|
HostNet: true,
|
||||||
|
HostAbstract: true,
|
||||||
|
RetainSession: true,
|
||||||
|
ForwardCancel: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nixos permissive defaults chromium", new(stubNixOS),
|
||||||
|
&hst.Config{
|
||||||
|
ID: "org.chromium.Chromium",
|
||||||
|
Args: []string{"zsh", "-c", "exec chromium "},
|
||||||
|
Identity: 9,
|
||||||
|
Groups: []string{"video"},
|
||||||
|
Username: "chronos",
|
||||||
|
Home: m("/home/chronos"),
|
||||||
|
SessionBus: &dbus.Config{
|
||||||
|
Talk: []string{
|
||||||
|
"org.freedesktop.Notifications",
|
||||||
|
"org.freedesktop.FileManager1",
|
||||||
|
"org.freedesktop.ScreenSaver",
|
||||||
|
"org.freedesktop.secrets",
|
||||||
|
"org.kde.kwalletd5",
|
||||||
|
"org.kde.kwalletd6",
|
||||||
|
"org.gnome.SessionManager",
|
||||||
|
},
|
||||||
|
Own: []string{
|
||||||
|
"org.chromium.Chromium.*",
|
||||||
|
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||||
|
"org.mpris.MediaPlayer2.chromium.*",
|
||||||
|
},
|
||||||
|
Call: map[string]string{
|
||||||
|
"org.freedesktop.portal.*": "*",
|
||||||
|
},
|
||||||
|
Broadcast: map[string]string{
|
||||||
|
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
|
||||||
|
},
|
||||||
|
Filter: true,
|
||||||
|
},
|
||||||
|
SystemBus: &dbus.Config{
|
||||||
|
Talk: []string{
|
||||||
|
"org.bluez",
|
||||||
|
"org.freedesktop.Avahi",
|
||||||
|
"org.freedesktop.UPower",
|
||||||
|
},
|
||||||
|
Filter: true,
|
||||||
|
},
|
||||||
|
Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse),
|
||||||
|
},
|
||||||
|
state.ID{
|
||||||
|
0xeb, 0xf0, 0x83, 0xd1,
|
||||||
|
0xb1, 0x75, 0x91, 0x17,
|
||||||
|
0x82, 0xd4, 0x13, 0x36,
|
||||||
|
0x9b, 0x64, 0xce, 0x7c,
|
||||||
|
},
|
||||||
|
system.New(context.TODO(), 1000009).
|
||||||
|
Ensure("/tmp/hakurei.0", 0711).
|
||||||
|
Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute).
|
||||||
|
Ensure("/tmp/hakurei.0/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/9", acl.Read, acl.Write, acl.Execute).
|
||||||
|
Ensure("/tmp/hakurei.0/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir", acl.Execute).
|
||||||
|
Ensure("/tmp/hakurei.0/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir/9", acl.Read, acl.Write, acl.Execute).
|
||||||
|
Ephemeral(system.Process, "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c", 0711).
|
||||||
|
Wayland(new(*os.File), "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
|
||||||
|
Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute).
|
||||||
|
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
||||||
|
Ephemeral(system.Process, "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c", acl.Execute).
|
||||||
|
Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse").
|
||||||
|
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
|
||||||
|
MustProxyDBus("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
|
||||||
|
Talk: []string{
|
||||||
|
"org.freedesktop.Notifications",
|
||||||
|
"org.freedesktop.FileManager1",
|
||||||
|
"org.freedesktop.ScreenSaver",
|
||||||
|
"org.freedesktop.secrets",
|
||||||
|
"org.kde.kwalletd5",
|
||||||
|
"org.kde.kwalletd6",
|
||||||
|
"org.gnome.SessionManager",
|
||||||
|
},
|
||||||
|
Own: []string{
|
||||||
|
"org.chromium.Chromium.*",
|
||||||
|
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||||
|
"org.mpris.MediaPlayer2.chromium.*",
|
||||||
|
},
|
||||||
|
Call: map[string]string{
|
||||||
|
"org.freedesktop.portal.*": "*",
|
||||||
|
},
|
||||||
|
Broadcast: map[string]string{
|
||||||
|
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
|
||||||
|
},
|
||||||
|
Filter: true,
|
||||||
|
}, "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
|
||||||
|
Talk: []string{
|
||||||
|
"org.bluez",
|
||||||
|
"org.freedesktop.Avahi",
|
||||||
|
"org.freedesktop.UPower",
|
||||||
|
},
|
||||||
|
Filter: true,
|
||||||
|
}).
|
||||||
|
UpdatePerm("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
|
||||||
|
UpdatePerm("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
|
||||||
|
&container.Params{
|
||||||
|
Dir: m("/home/chronos"),
|
||||||
|
Path: m("/run/current-system/sw/bin/zsh"),
|
||||||
|
Args: []string{"zsh", "-c", "exec chromium "},
|
||||||
|
Env: []string{
|
||||||
|
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
|
||||||
|
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
|
||||||
|
"HOME=/home/chronos",
|
||||||
|
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
|
||||||
|
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
|
||||||
|
"SHELL=/run/current-system/sw/bin/zsh",
|
||||||
|
"TERM=xterm-256color",
|
||||||
|
"USER=chronos",
|
||||||
|
"WAYLAND_DISPLAY=wayland-0",
|
||||||
|
"XDG_RUNTIME_DIR=/run/user/65534",
|
||||||
|
"XDG_SESSION_CLASS=user",
|
||||||
|
"XDG_SESSION_TYPE=tty",
|
||||||
|
},
|
||||||
|
Ops: new(container.Ops).
|
||||||
|
Root(m("/"), container.BindWritable).
|
||||||
|
Proc(m("/proc/")).
|
||||||
|
Tmpfs(hst.AbsTmp, 4096, 0755).
|
||||||
|
DevWritable(m("/dev/"), true).
|
||||||
|
Tmpfs(m("/dev/shm"), 0, 01777).
|
||||||
|
Bind(m("/dev/dri"), m("/dev/dri"), container.BindWritable|container.BindDevice|container.BindOptional).
|
||||||
|
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
|
||||||
|
Readonly(m("/var/run/nscd"), 0755).
|
||||||
|
Etc(m("/etc/"), "ebf083d1b175911782d413369b64ce7c").
|
||||||
|
Tmpfs(m("/run/user/1971"), 8192, 0755).
|
||||||
|
Tmpfs(m("/run/dbus"), 8192, 0755).
|
||||||
|
Remount(m("/dev/"), syscall.MS_RDONLY).
|
||||||
|
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||||
|
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/65534"), container.BindWritable).
|
||||||
|
Bind(m("/tmp/hakurei.0/tmpdir/9"), m("/tmp/"), container.BindWritable).
|
||||||
|
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
||||||
|
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
|
||||||
|
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0).
|
||||||
|
Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0).
|
||||||
|
Place(m(hst.Tmp+"/pulse-cookie"), nil).
|
||||||
|
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0).
|
||||||
|
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
|
||||||
|
Remount(m("/"), syscall.MS_RDONLY),
|
||||||
|
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
|
||||||
|
HostNet: true,
|
||||||
|
HostAbstract: true,
|
||||||
|
RetainSession: true,
|
||||||
|
ForwardCancel: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"nixos chromium direct wayland", new(stubNixOS),
|
||||||
|
&hst.Config{
|
||||||
|
ID: "org.chromium.Chromium",
|
||||||
|
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
|
||||||
|
Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse),
|
||||||
|
Shell: m("/run/current-system/sw/bin/zsh"),
|
||||||
|
|
||||||
|
Container: &hst.ContainerConfig{
|
||||||
|
Userns: true, HostNet: true, MapRealUID: true, Env: nil,
|
||||||
|
Filesystem: []hst.FilesystemConfigJSON{
|
||||||
|
f(&hst.FSBind{Source: m("/bin")}),
|
||||||
|
f(&hst.FSBind{Source: m("/usr/bin/")}),
|
||||||
|
f(&hst.FSBind{Source: m("/nix/store")}),
|
||||||
|
f(&hst.FSBind{Source: m("/run/current-system")}),
|
||||||
|
f(&hst.FSBind{Source: m("/sys/block"), Optional: true}),
|
||||||
|
f(&hst.FSBind{Source: m("/sys/bus"), Optional: true}),
|
||||||
|
f(&hst.FSBind{Source: m("/sys/class"), Optional: true}),
|
||||||
|
f(&hst.FSBind{Source: m("/sys/dev"), Optional: true}),
|
||||||
|
f(&hst.FSBind{Source: m("/sys/devices"), Optional: true}),
|
||||||
|
f(&hst.FSBind{Source: m("/run/opengl-driver")}),
|
||||||
|
f(&hst.FSBind{Source: m("/dev/dri"), Device: true, Optional: true}),
|
||||||
|
f(&hst.FSBind{Source: m("/etc/"), Target: m("/etc/"), Special: true}),
|
||||||
|
f(&hst.FSBind{Source: m("/var/lib/persist/module/hakurei/0/1"), Write: true, Ensure: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SystemBus: &dbus.Config{
|
||||||
|
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
|
||||||
|
Filter: true,
|
||||||
|
},
|
||||||
|
SessionBus: &dbus.Config{
|
||||||
|
Talk: []string{
|
||||||
|
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
|
||||||
|
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
|
||||||
|
"org.kde.kwalletd5", "org.kde.kwalletd6",
|
||||||
|
},
|
||||||
|
Own: []string{
|
||||||
|
"org.chromium.Chromium.*",
|
||||||
|
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||||
|
"org.mpris.MediaPlayer2.chromium.*",
|
||||||
|
},
|
||||||
|
Call: map[string]string{}, Broadcast: map[string]string{},
|
||||||
|
Filter: true,
|
||||||
|
},
|
||||||
|
DirectWayland: true,
|
||||||
|
|
||||||
|
Username: "u0_a1",
|
||||||
|
Home: m("/var/lib/persist/module/hakurei/0/1"),
|
||||||
|
Identity: 1, Groups: []string{},
|
||||||
|
},
|
||||||
|
state.ID{
|
||||||
|
0x8e, 0x2c, 0x76, 0xb0,
|
||||||
|
0x66, 0xda, 0xbe, 0x57,
|
||||||
|
0x4c, 0xf0, 0x73, 0xbd,
|
||||||
|
0xb4, 0x6e, 0xb5, 0xc1,
|
||||||
|
},
|
||||||
|
system.New(context.TODO(), 1000001).
|
||||||
|
Ensure("/tmp/hakurei.0", 0711).
|
||||||
|
Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute).
|
||||||
|
Ensure("/tmp/hakurei.0/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/1", acl.Read, acl.Write, acl.Execute).
|
||||||
|
Ensure("/tmp/hakurei.0/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir", acl.Execute).
|
||||||
|
Ensure("/tmp/hakurei.0/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir/1", acl.Read, acl.Write, acl.Execute).
|
||||||
|
Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute).
|
||||||
|
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
||||||
|
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
|
||||||
|
Ephemeral(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
|
||||||
|
Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
|
||||||
|
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
|
||||||
|
Ephemeral(system.Process, "/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
|
||||||
|
MustProxyDBus("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
|
||||||
|
Talk: []string{
|
||||||
|
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
|
||||||
|
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
|
||||||
|
"org.kde.kwalletd5", "org.kde.kwalletd6",
|
||||||
|
},
|
||||||
|
Own: []string{
|
||||||
|
"org.chromium.Chromium.*",
|
||||||
|
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||||
|
"org.mpris.MediaPlayer2.chromium.*",
|
||||||
|
},
|
||||||
|
Call: map[string]string{}, Broadcast: map[string]string{},
|
||||||
|
Filter: true,
|
||||||
|
}, "/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
|
||||||
|
Talk: []string{
|
||||||
|
"org.bluez",
|
||||||
|
"org.freedesktop.Avahi",
|
||||||
|
"org.freedesktop.UPower",
|
||||||
|
},
|
||||||
|
Filter: true,
|
||||||
|
}).
|
||||||
|
UpdatePerm("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
|
||||||
|
UpdatePerm("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
|
||||||
|
&container.Params{
|
||||||
|
Uid: 1971,
|
||||||
|
Gid: 100,
|
||||||
|
Dir: m("/var/lib/persist/module/hakurei/0/1"),
|
||||||
|
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
|
||||||
|
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
|
||||||
|
Env: []string{
|
||||||
|
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
|
||||||
|
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
|
||||||
|
"HOME=/var/lib/persist/module/hakurei/0/1",
|
||||||
|
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
|
||||||
|
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
|
||||||
|
"SHELL=/run/current-system/sw/bin/zsh",
|
||||||
|
"TERM=xterm-256color",
|
||||||
|
"USER=u0_a1",
|
||||||
|
"WAYLAND_DISPLAY=wayland-0",
|
||||||
|
"XDG_RUNTIME_DIR=/run/user/1971",
|
||||||
|
"XDG_SESSION_CLASS=user",
|
||||||
|
"XDG_SESSION_TYPE=tty",
|
||||||
|
},
|
||||||
|
Ops: new(container.Ops).
|
||||||
|
Proc(m("/proc/")).
|
||||||
|
Tmpfs(hst.AbsTmp, 4096, 0755).
|
||||||
|
DevWritable(m("/dev/"), true).
|
||||||
|
Tmpfs(m("/dev/shm"), 0, 01777).
|
||||||
|
Bind(m("/bin"), m("/bin"), 0).
|
||||||
|
Bind(m("/usr/bin/"), m("/usr/bin/"), 0).
|
||||||
|
Bind(m("/nix/store"), m("/nix/store"), 0).
|
||||||
|
Bind(m("/run/current-system"), m("/run/current-system"), 0).
|
||||||
|
Bind(m("/sys/block"), m("/sys/block"), container.BindOptional).
|
||||||
|
Bind(m("/sys/bus"), m("/sys/bus"), container.BindOptional).
|
||||||
|
Bind(m("/sys/class"), m("/sys/class"), container.BindOptional).
|
||||||
|
Bind(m("/sys/dev"), m("/sys/dev"), container.BindOptional).
|
||||||
|
Bind(m("/sys/devices"), m("/sys/devices"), container.BindOptional).
|
||||||
|
Bind(m("/run/opengl-driver"), m("/run/opengl-driver"), 0).
|
||||||
|
Bind(m("/dev/dri"), m("/dev/dri"), container.BindDevice|container.BindWritable|container.BindOptional).
|
||||||
|
Etc(m("/etc/"), "8e2c76b066dabe574cf073bdb46eb5c1").
|
||||||
|
Bind(m("/var/lib/persist/module/hakurei/0/1"), m("/var/lib/persist/module/hakurei/0/1"), container.BindWritable|container.BindEnsure).
|
||||||
|
Remount(m("/dev/"), syscall.MS_RDONLY).
|
||||||
|
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||||
|
Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), container.BindWritable).
|
||||||
|
Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), container.BindWritable).
|
||||||
|
Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")).
|
||||||
|
Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
|
||||||
|
Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0).
|
||||||
|
Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0).
|
||||||
|
Place(m(hst.Tmp+"/pulse-cookie"), nil).
|
||||||
|
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
|
||||||
|
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
|
||||||
|
Remount(m("/"), syscall.MS_RDONLY),
|
||||||
|
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel,
|
||||||
|
HostNet: true,
|
||||||
|
ForwardCancel: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Run("finalise", func(t *testing.T) {
|
||||||
|
seal := outcome{syscallDispatcher: tc.k, id: &stringPair[state.ID]{tc.id, tc.id.String()}}
|
||||||
|
err := seal.finalise(t.Context(), tc.config)
|
||||||
|
if err != nil {
|
||||||
|
if s, ok := container.GetErrorMessage(err); !ok {
|
||||||
|
t.Fatalf("Seal: error = %v", err)
|
||||||
|
} else {
|
||||||
|
t.Fatalf("Seal: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("sys", func(t *testing.T) {
|
||||||
|
if !seal.sys.Equal(tc.wantSys) {
|
||||||
|
t.Errorf("Seal: sys = %#v, want %#v", seal.sys, tc.wantSys)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("params", func(t *testing.T) {
|
||||||
|
if !reflect.DeepEqual(seal.container, tc.wantParams) {
|
||||||
|
t.Errorf("seal: container =\n%s\n, want\n%s", mustMarshal(seal.container), mustMarshal(tc.wantParams))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustMarshal(v any) string {
|
||||||
|
if b, err := json.Marshal(v); err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
} else {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stubDirEntries(names ...string) (e []fs.DirEntry, err error) {
|
||||||
|
e = make([]fs.DirEntry, len(names))
|
||||||
|
for i, name := range names {
|
||||||
|
e[i] = stubDirEntryPath(name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type stubDirEntryPath string
|
||||||
|
|
||||||
|
func (p stubDirEntryPath) Name() string { return string(p) }
|
||||||
|
func (p stubDirEntryPath) IsDir() bool { panic("attempted to call IsDir") }
|
||||||
|
func (p stubDirEntryPath) Type() fs.FileMode { panic("attempted to call Type") }
|
||||||
|
func (p stubDirEntryPath) Info() (fs.FileInfo, error) { panic("attempted to call Info") }
|
||||||
|
|
||||||
|
type stubFileInfoMode fs.FileMode
|
||||||
|
|
||||||
|
func (s stubFileInfoMode) Name() string { panic("attempted to call Name") }
|
||||||
|
func (s stubFileInfoMode) Size() int64 { panic("attempted to call Size") }
|
||||||
|
func (s stubFileInfoMode) Mode() fs.FileMode { return fs.FileMode(s) }
|
||||||
|
func (s stubFileInfoMode) ModTime() time.Time { panic("attempted to call ModTime") }
|
||||||
|
func (s stubFileInfoMode) IsDir() bool { panic("attempted to call IsDir") }
|
||||||
|
func (s stubFileInfoMode) Sys() any { panic("attempted to call Sys") }
|
||||||
|
|
||||||
|
type stubFileInfoIsDir bool
|
||||||
|
|
||||||
|
func (s stubFileInfoIsDir) Name() string { panic("attempted to call Name") }
|
||||||
|
func (s stubFileInfoIsDir) Size() int64 { panic("attempted to call Size") }
|
||||||
|
func (s stubFileInfoIsDir) Mode() fs.FileMode { panic("attempted to call Mode") }
|
||||||
|
func (s stubFileInfoIsDir) ModTime() time.Time { panic("attempted to call ModTime") }
|
||||||
|
func (s stubFileInfoIsDir) IsDir() bool { return bool(s) }
|
||||||
|
func (s stubFileInfoIsDir) Sys() any { panic("attempted to call Sys") }
|
||||||
|
|
||||||
|
func m(pathname string) *container.Absolute {
|
||||||
|
return container.MustAbs(pathname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func f(c hst.FilesystemConfig) hst.FilesystemConfigJSON {
|
||||||
|
return hst.FilesystemConfigJSON{FilesystemConfig: c}
|
||||||
|
}
|
||||||
@ -11,20 +11,23 @@ import (
|
|||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
"hakurei.app/container/seccomp"
|
"hakurei.app/container/seccomp"
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/internal/hlog"
|
|
||||||
"hakurei.app/internal/sys"
|
|
||||||
"hakurei.app/system/dbus"
|
"hakurei.app/system/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// in practice there should be less than 30 entries added by the runtime;
|
// in practice there should be less than 30 system mount points
|
||||||
// allocating slightly more as a margin for future expansion
|
|
||||||
const preallocateOpsCount = 1 << 5
|
const preallocateOpsCount = 1 << 5
|
||||||
|
|
||||||
// newContainer initialises [container.Params] via [hst.ContainerConfig].
|
// newContainer initialises [container.Params] via [hst.ContainerConfig].
|
||||||
// Note that remaining container setup must be queued by the caller.
|
// Note that remaining container setup must be queued by the caller.
|
||||||
func newContainer(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 {
|
if s == nil {
|
||||||
return nil, nil, hlog.WrapErr(syscall.EBADE, "invalid container configuration")
|
return nil, nil, newWithMessage("invalid container configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
params := &container.Params{
|
params := &container.Params{
|
||||||
@ -40,9 +43,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
|||||||
ForwardCancel: s.WaitDelay >= 0,
|
ForwardCancel: s.WaitDelay >= 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
as := &hst.ApplyState{
|
as := &hst.ApplyState{AutoEtcPrefix: prefix}
|
||||||
AutoEtcPrefix: prefix,
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem))
|
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem))
|
||||||
params.Ops = &ops
|
params.Ops = &ops
|
||||||
@ -67,15 +68,13 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.MapRealUID {
|
if s.MapRealUID {
|
||||||
/* some programs fail to connect to dbus session running as a different uid
|
params.Uid = k.getuid()
|
||||||
so this workaround is introduced to map priv-side caller uid in container */
|
|
||||||
params.Uid = os.Getuid()
|
|
||||||
*uid = params.Uid
|
*uid = params.Uid
|
||||||
params.Gid = os.Getgid()
|
params.Gid = k.getgid()
|
||||||
*gid = params.Gid
|
*gid = params.Gid
|
||||||
} else {
|
} else {
|
||||||
*uid = container.OverflowUid()
|
*uid = k.overflowUid()
|
||||||
*gid = container.OverflowGid()
|
*gid = k.overflowGid()
|
||||||
}
|
}
|
||||||
|
|
||||||
filesystem := s.Filesystem
|
filesystem := s.Filesystem
|
||||||
@ -102,13 +101,15 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
|||||||
} else {
|
} else {
|
||||||
params.Bind(container.AbsFHSDev, container.AbsFHSDev, container.BindWritable|container.BindDevice)
|
params.Bind(container.AbsFHSDev, container.AbsFHSDev, container.BindWritable|container.BindDevice)
|
||||||
}
|
}
|
||||||
|
// /dev is mounted readonly later on, this prevents /dev/shm from going readonly with it
|
||||||
|
params.Tmpfs(container.AbsFHSDev.Append("shm"), 0, 01777)
|
||||||
|
|
||||||
/* retrieve paths and hide them if they're made available in the sandbox;
|
/* retrieve paths and hide them if they're made available in the sandbox;
|
||||||
|
|
||||||
this feature tries to improve user experience of permissive defaults, and
|
this feature tries to improve user experience of permissive defaults, and
|
||||||
to warn about issues in custom configuration; it is NOT a security feature
|
to warn about issues in custom configuration; it is NOT a security feature
|
||||||
and should not be treated as such, ALWAYS be careful with what you bind */
|
and should not be treated as such, ALWAYS be careful with what you bind */
|
||||||
var hidePaths []string
|
var hidePaths []string
|
||||||
sc := os.Paths()
|
|
||||||
hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String())
|
hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String())
|
||||||
_, systemBusAddr := dbus.Address()
|
_, systemBusAddr := dbus.Address()
|
||||||
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
|
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
|
||||||
@ -125,11 +126,11 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
|||||||
// get parent dir of socket
|
// get parent dir of socket
|
||||||
dir := path.Dir(pair[1])
|
dir := path.Dir(pair[1])
|
||||||
if dir == "." || dir == container.FHSRoot {
|
if dir == "." || dir == container.FHSRoot {
|
||||||
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)
|
hidePaths = append(hidePaths, dir)
|
||||||
} else {
|
} else {
|
||||||
os.Printf("dbus socket %q is not absolute", pair[1])
|
k.verbosef("dbus socket %q is not absolute", pair[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,7 +138,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
|||||||
}
|
}
|
||||||
hidePathMatch := make([]bool, len(hidePaths))
|
hidePathMatch := make([]bool, len(hidePaths))
|
||||||
for i := range 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
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,7 +157,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
|||||||
// AutoRootOp is a collection of many BindMountOp internally
|
// AutoRootOp is a collection of many BindMountOp internally
|
||||||
var autoRootEntries []fs.DirEntry
|
var autoRootEntries []fs.DirEntry
|
||||||
if autoroot != nil {
|
if autoroot != nil {
|
||||||
if d, err := os.ReadDir(autoroot.Source.String()); err != nil {
|
if d, err := k.readdir(autoroot.Source.String()); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
} else {
|
} else {
|
||||||
// autoroot counter
|
// autoroot counter
|
||||||
@ -192,7 +193,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
|||||||
}
|
}
|
||||||
|
|
||||||
hidePathSourceEval[i] = [2]string{a.String(), a.String()}
|
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
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,7 +209,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
} else if ok {
|
} else if ok {
|
||||||
hidePathMatch[i] = true
|
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])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -233,20 +234,19 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
|
|||||||
|
|
||||||
// no more ContainerConfig paths beyond this point
|
// no more ContainerConfig paths beyond this point
|
||||||
if !s.Device {
|
if !s.Device {
|
||||||
params.
|
params.Remount(container.AbsFHSDev, syscall.MS_RDONLY)
|
||||||
Remount(container.AbsFHSDev, syscall.MS_RDONLY).
|
|
||||||
Tmpfs(container.AbsFHSDev.Append("shm"), 0, 01777)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return params, maps.Clone(s.Env), nil
|
return params, maps.Clone(s.Env), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func evalSymlinks(os sys.State, v *string) error {
|
// evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist].
|
||||||
if p, err := os.EvalSymlinks(*v); err != nil {
|
func evalSymlinks(k syscallDispatcher, v *string) error {
|
||||||
|
if p, err := k.evalSymlinks(*v); err != nil {
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
os.Printf("path %q does not yet exist", *v)
|
k.verbosef("path %q does not yet exist", *v)
|
||||||
} else {
|
} else {
|
||||||
*v = p
|
*v = p
|
||||||
}
|
}
|
||||||
99
internal/app/dispatcher.go
Normal file
99
internal/app/dispatcher.go
Normal file
@ -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,24 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"hakurei.app/container"
|
|
||||||
"hakurei.app/internal/app/state"
|
|
||||||
"hakurei.app/internal/sys"
|
|
||||||
"hakurei.app/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewWithID(id state.ID, os sys.State) App {
|
|
||||||
a := new(app)
|
|
||||||
a.id = newID(&id)
|
|
||||||
a.sys = os
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
func AppIParams(a App, sa SealedApp) (*system.I, *container.Params) {
|
|
||||||
v := a.(*app)
|
|
||||||
seal := sa.(*outcome)
|
|
||||||
if v.outcome != seal || v.id != seal.id {
|
|
||||||
panic("broken app/outcome link")
|
|
||||||
}
|
|
||||||
return seal.sys, seal.container
|
|
||||||
}
|
|
||||||
@ -9,8 +9,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"os/user"
|
||||||
"regexp"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -20,49 +19,20 @@ import (
|
|||||||
|
|
||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/internal"
|
|
||||||
"hakurei.app/internal/app/state"
|
"hakurei.app/internal/app/state"
|
||||||
"hakurei.app/internal/hlog"
|
"hakurei.app/internal/hlog"
|
||||||
"hakurei.app/internal/sys"
|
|
||||||
"hakurei.app/system"
|
"hakurei.app/system"
|
||||||
"hakurei.app/system/acl"
|
"hakurei.app/system/acl"
|
||||||
"hakurei.app/system/dbus"
|
"hakurei.app/system/dbus"
|
||||||
"hakurei.app/system/wayland"
|
"hakurei.app/system/wayland"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func newWithMessage(msg string) error { return newWithMessageError(msg, os.ErrInvalid) }
|
||||||
home = "HOME"
|
func newWithMessageError(msg string, err error) error {
|
||||||
shell = "SHELL"
|
return &hst.AppError{Step: "finalise", Err: err, Msg: msg}
|
||||||
|
}
|
||||||
|
|
||||||
xdgConfigHome = "XDG_CONFIG_HOME"
|
// An outcome is the runnable state of a hakurei container via [hst.Config].
|
||||||
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
|
||||||
xdgSessionClass = "XDG_SESSION_CLASS"
|
|
||||||
xdgSessionType = "XDG_SESSION_TYPE"
|
|
||||||
|
|
||||||
term = "TERM"
|
|
||||||
display = "DISPLAY"
|
|
||||||
|
|
||||||
pulseServer = "PULSE_SERVER"
|
|
||||||
pulseCookie = "PULSE_COOKIE"
|
|
||||||
|
|
||||||
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
|
|
||||||
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrIdent = errors.New("invalid identity")
|
|
||||||
ErrName = errors.New("invalid username")
|
|
||||||
|
|
||||||
ErrXDisplay = errors.New(display + " unset")
|
|
||||||
|
|
||||||
ErrPulseCookie = errors.New("pulse cookie not present")
|
|
||||||
ErrPulseSocket = errors.New("pulse socket not present")
|
|
||||||
ErrPulseMode = errors.New("unexpected pulse socket mode")
|
|
||||||
)
|
|
||||||
|
|
||||||
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$")
|
|
||||||
|
|
||||||
// outcome stores copies of various parts of [hst.Config]
|
|
||||||
type outcome struct {
|
type outcome struct {
|
||||||
// copied from initialising [app]
|
// copied from initialising [app]
|
||||||
id *stringPair[state.ID]
|
id *stringPair[state.ID]
|
||||||
@ -83,8 +53,9 @@ type outcome struct {
|
|||||||
container *container.Params
|
container *container.Params
|
||||||
env map[string]string
|
env map[string]string
|
||||||
sync *os.File
|
sync *os.File
|
||||||
|
active atomic.Bool
|
||||||
|
|
||||||
f atomic.Bool
|
syscallDispatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
// shareHost holds optional share directory state that must not be accessed directly
|
// shareHost holds optional share directory state that must not be accessed directly
|
||||||
@ -136,8 +107,7 @@ func (share *shareHost) runtime() *container.Absolute {
|
|||||||
|
|
||||||
// hsuUser stores post-hsu credentials and metadata
|
// hsuUser stores post-hsu credentials and metadata
|
||||||
type hsuUser struct {
|
type hsuUser struct {
|
||||||
// identity
|
identity *stringPair[int]
|
||||||
aid *stringPair[int]
|
|
||||||
// target uid resolved by hid:aid
|
// target uid resolved by hid:aid
|
||||||
uid *stringPair[int]
|
uid *stringPair[int]
|
||||||
|
|
||||||
@ -150,59 +120,82 @@ type hsuUser struct {
|
|||||||
username string
|
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 {
|
||||||
if seal.ctx != nil {
|
const (
|
||||||
panic("finalise called twice")
|
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")
|
||||||
}
|
}
|
||||||
seal.ctx = ctx
|
if k.ctx != nil {
|
||||||
|
// unreachable
|
||||||
|
panic("attempting to finalise twice")
|
||||||
|
}
|
||||||
|
k.ctx = ctx
|
||||||
|
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return hlog.WrapErr(syscall.EINVAL, syscall.EINVAL.Error())
|
return newWithMessage("invalid configuration")
|
||||||
}
|
}
|
||||||
if config.Home == nil {
|
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
|
// encode initial configuration for state tracking
|
||||||
ct := new(bytes.Buffer)
|
ct := new(bytes.Buffer)
|
||||||
if err := gob.NewEncoder(ct).Encode(config); err != nil {
|
if err := gob.NewEncoder(ct).Encode(config); err != nil {
|
||||||
return hlog.WrapErrSuffix(err,
|
return &hst.AppError{Step: "encode initial config", Err: err}
|
||||||
"cannot encode initial config:")
|
|
||||||
}
|
}
|
||||||
seal.ct = ct
|
k.ct = ct
|
||||||
}
|
}
|
||||||
|
|
||||||
// allowed aid range 0 to 9999, this is checked again in hsu
|
// allowed identity range 0 to 9999, this is checked again in hsu
|
||||||
if config.Identity < 0 || config.Identity > 9999 {
|
if config.Identity < 0 || config.Identity > 9999 {
|
||||||
return hlog.WrapErr(ErrIdent,
|
return newWithMessage(fmt.Sprintf("identity %d out of range", config.Identity))
|
||||||
fmt.Sprintf("identity %d out of range", config.Identity))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
seal.user = hsuUser{
|
k.user = hsuUser{
|
||||||
aid: newInt(config.Identity),
|
identity: newInt(config.Identity),
|
||||||
home: config.Home,
|
home: config.Home,
|
||||||
username: config.Username,
|
username: config.Username,
|
||||||
}
|
}
|
||||||
if seal.user.username == "" {
|
|
||||||
seal.user.username = "chronos"
|
hsu := Hsu{k: k}
|
||||||
} else if !posixUsername.MatchString(seal.user.username) ||
|
if k.user.username == "" {
|
||||||
len(seal.user.username) >= internal.Sysconf(internal.SC_LOGIN_NAME_MAX) {
|
k.user.username = "chronos"
|
||||||
return hlog.WrapErr(ErrName,
|
} else if !isValidUsername(k.user.username) {
|
||||||
fmt.Sprintf("invalid user name %q", seal.user.username))
|
return newWithMessage(fmt.Sprintf("invalid user name %q", k.user.username))
|
||||||
}
|
}
|
||||||
if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil {
|
k.user.uid = newInt(HsuUid(hsu.MustID(), k.user.identity.unwrap()))
|
||||||
return err
|
|
||||||
} else {
|
k.user.supp = make([]string, len(config.Groups))
|
||||||
seal.user.uid = newInt(u)
|
|
||||||
}
|
|
||||||
seal.user.supp = make([]string, len(config.Groups))
|
|
||||||
for i, name := range config.Groups {
|
for i, name := range config.Groups {
|
||||||
if g, err := sys.LookupGroup(name); err != nil {
|
if gid, err := k.lookupGroupId(name); err != nil {
|
||||||
return hlog.WrapErr(err,
|
var unknownGroupError user.UnknownGroupError
|
||||||
fmt.Sprintf("unknown group %q", name))
|
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 {
|
} else {
|
||||||
seal.user.supp[i] = g.Gid
|
k.user.supp[i] = gid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +205,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
|||||||
|
|
||||||
if config.Shell == nil {
|
if config.Shell == nil {
|
||||||
config.Shell = container.AbsFHSRoot.Append("bin", "sh")
|
config.Shell = container.AbsFHSRoot.Append("bin", "sh")
|
||||||
s, _ := sys.LookupEnv(shell)
|
s, _ := k.lookupEnv(shell)
|
||||||
if a, err := container.NewAbs(s); err == nil {
|
if a, err := container.NewAbs(s); err == nil {
|
||||||
config.Shell = a
|
config.Shell = a
|
||||||
}
|
}
|
||||||
@ -221,10 +214,10 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
|||||||
// hsu clears the environment so resolve paths early
|
// hsu clears the environment so resolve paths early
|
||||||
if config.Path == nil {
|
if config.Path == nil {
|
||||||
if len(config.Args) > 0 {
|
if len(config.Args) > 0 {
|
||||||
if p, err := sys.LookPath(config.Args[0]); err != nil {
|
if p, err := k.lookPath(config.Args[0]); err != nil {
|
||||||
return hlog.WrapErr(err, err.Error())
|
return &hst.AppError{Step: "look up executable file", Err: err}
|
||||||
} else if config.Path, err = container.NewAbs(p); err != nil {
|
} else if config.Path, err = container.NewAbs(p); err != nil {
|
||||||
return hlog.WrapErr(err, err.Error())
|
return newWithMessageError(err.Error(), err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
config.Path = config.Shell
|
config.Path = config.Shell
|
||||||
@ -239,7 +232,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
|||||||
|
|
||||||
Filesystem: []hst.FilesystemConfigJSON{
|
Filesystem: []hst.FilesystemConfigJSON{
|
||||||
// autoroot, includes the home directory
|
// autoroot, includes the home directory
|
||||||
{&hst.FSBind{
|
{FilesystemConfig: &hst.FSBind{
|
||||||
Target: container.AbsFHSRoot,
|
Target: container.AbsFHSRoot,
|
||||||
Source: container.AbsFHSRoot,
|
Source: container.AbsFHSRoot,
|
||||||
Write: true,
|
Write: true,
|
||||||
@ -257,7 +250,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
|||||||
|
|
||||||
// hide nscd from container if present
|
// hide nscd from container if present
|
||||||
nscd := container.AbsFHSVar.Append("run/nscd")
|
nscd := container.AbsFHSVar.Append("run/nscd")
|
||||||
if _, err := 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}})
|
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,93 +268,95 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
|||||||
|
|
||||||
// late nil checks for pd behaviour
|
// late nil checks for pd behaviour
|
||||||
if config.Shell == nil {
|
if config.Shell == nil {
|
||||||
return hlog.WrapErr(syscall.EINVAL, "invalid shell path")
|
return newWithMessage("invalid shell path")
|
||||||
}
|
}
|
||||||
if config.Path == nil {
|
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 mapuid, mapgid *stringPair[int]
|
||||||
{
|
{
|
||||||
var uid, gid int
|
var uid, gid int
|
||||||
var err error
|
var err error
|
||||||
seal.container, seal.env, err = newContainer(config.Container, sys, seal.id.String(), &uid, &gid)
|
k.container, k.env, err = newContainer(k, config.Container, k.id.String(), &share.sc, &uid, &gid)
|
||||||
seal.waitDelay = config.Container.WaitDelay
|
k.waitDelay = config.Container.WaitDelay
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return hlog.WrapErrSuffix(err,
|
return &hst.AppError{Step: "initialise container configuration", Err: err}
|
||||||
"cannot initialise container configuration:")
|
|
||||||
}
|
}
|
||||||
if len(config.Args) == 0 {
|
if len(config.Args) == 0 {
|
||||||
config.Args = []string{config.Path.String()}
|
config.Args = []string{config.Path.String()}
|
||||||
}
|
}
|
||||||
seal.container.Path = config.Path
|
k.container.Path = config.Path
|
||||||
seal.container.Args = config.Args
|
k.container.Args = config.Args
|
||||||
|
|
||||||
mapuid = newInt(uid)
|
mapuid = newInt(uid)
|
||||||
mapgid = newInt(gid)
|
mapgid = newInt(gid)
|
||||||
if seal.env == nil {
|
if k.env == nil {
|
||||||
seal.env = make(map[string]string, 1<<6)
|
k.env = make(map[string]string, 1<<6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
|
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
|
||||||
innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String())
|
innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String())
|
||||||
seal.env[xdgRuntimeDir] = innerRuntimeDir.String()
|
k.env[xdgRuntimeDir] = innerRuntimeDir.String()
|
||||||
seal.env[xdgSessionClass] = "user"
|
k.env[xdgSessionClass] = "user"
|
||||||
seal.env[xdgSessionType] = "tty"
|
k.env[xdgSessionType] = "tty"
|
||||||
|
|
||||||
share := &shareHost{seal: seal, sc: sys.Paths()}
|
k.runDirPath = share.sc.RunDirPath
|
||||||
seal.runDirPath = share.sc.RunDirPath
|
k.sys = system.New(k.ctx, k.user.uid.unwrap())
|
||||||
seal.sys = system.New(seal.user.uid.unwrap())
|
k.sys.Ensure(share.sc.SharePath.String(), 0711)
|
||||||
seal.sys.Ensure(share.sc.SharePath.String(), 0711)
|
|
||||||
|
|
||||||
{
|
{
|
||||||
runtimeDir := share.sc.SharePath.Append("runtime")
|
runtimeDir := share.sc.SharePath.Append("runtime")
|
||||||
seal.sys.Ensure(runtimeDir.String(), 0700)
|
k.sys.Ensure(runtimeDir.String(), 0700)
|
||||||
seal.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute)
|
k.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute)
|
||||||
runtimeDirInst := runtimeDir.Append(seal.user.aid.String())
|
runtimeDirInst := runtimeDir.Append(k.user.identity.String())
|
||||||
seal.sys.Ensure(runtimeDirInst.String(), 0700)
|
k.sys.Ensure(runtimeDirInst.String(), 0700)
|
||||||
seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute)
|
k.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute)
|
||||||
seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
|
k.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
|
||||||
seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
|
k.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
tmpdir := share.sc.SharePath.Append("tmpdir")
|
tmpdir := share.sc.SharePath.Append("tmpdir")
|
||||||
seal.sys.Ensure(tmpdir.String(), 0700)
|
k.sys.Ensure(tmpdir.String(), 0700)
|
||||||
seal.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute)
|
k.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute)
|
||||||
tmpdirInst := tmpdir.Append(seal.user.aid.String())
|
tmpdirInst := tmpdir.Append(k.user.identity.String())
|
||||||
seal.sys.Ensure(tmpdirInst.String(), 01700)
|
k.sys.Ensure(tmpdirInst.String(), 01700)
|
||||||
seal.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute)
|
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
|
// 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"
|
username := "chronos"
|
||||||
if seal.user.username != "" {
|
if k.user.username != "" {
|
||||||
username = seal.user.username
|
username = k.user.username
|
||||||
}
|
}
|
||||||
seal.container.Dir = seal.user.home
|
k.container.Dir = k.user.home
|
||||||
seal.env["HOME"] = seal.user.home.String()
|
k.env["HOME"] = k.user.home.String()
|
||||||
seal.env["USER"] = username
|
k.env["USER"] = username
|
||||||
seal.env[shell] = config.Shell.String()
|
k.env[shell] = config.Shell.String()
|
||||||
|
|
||||||
seal.container.Place(container.AbsFHSEtc.Append("passwd"),
|
k.container.Place(container.AbsFHSEtc.Append("passwd"),
|
||||||
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+seal.user.home.String()+":"+config.Shell.String()+"\n"))
|
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+k.user.home.String()+":"+config.Shell.String()+"\n"))
|
||||||
seal.container.Place(container.AbsFHSEtc.Append("group"),
|
k.container.Place(container.AbsFHSEtc.Append("group"),
|
||||||
[]byte("hakurei:x:"+mapgid.String()+":\n"))
|
[]byte("hakurei:x:"+mapgid.String()+":\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// pass TERM for proper terminal I/O in initial process
|
// pass TERM for proper terminal I/O in initial process
|
||||||
if t, ok := sys.LookupEnv(term); ok {
|
if t, ok := k.lookupEnv(term); ok {
|
||||||
seal.env[term] = t
|
k.env[term] = t
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Enablements.Unwrap()&system.EWayland != 0 {
|
if config.Enablements.Unwrap()&system.EWayland != 0 {
|
||||||
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
|
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
|
||||||
var socketPath *container.Absolute
|
var socketPath *container.Absolute
|
||||||
if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok {
|
if name, ok := k.lookupEnv(wayland.WaylandDisplay); !ok {
|
||||||
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
|
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
|
||||||
socketPath = share.sc.RuntimePath.Append(wayland.FallbackName)
|
socketPath = share.sc.RuntimePath.Append(wayland.FallbackName)
|
||||||
} else if a, err := container.NewAbs(name); err != nil {
|
} else if a, err := container.NewAbs(name); err != nil {
|
||||||
@ -371,30 +366,29 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
innerPath := innerRuntimeDir.Append(wayland.FallbackName)
|
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
|
if !config.DirectWayland { // set up security-context-v1
|
||||||
appID := config.ID
|
appID := config.ID
|
||||||
if appID == "" {
|
if appID == "" {
|
||||||
// use instance ID in case app id is not set
|
// use instance ID in case app id is not set
|
||||||
appID = "app.hakurei." + seal.id.String()
|
appID = "app.hakurei." + k.id.String()
|
||||||
}
|
}
|
||||||
// downstream socket paths
|
// downstream socket paths
|
||||||
outerPath := share.instance().Append("wayland")
|
outerPath := share.instance().Append("wayland")
|
||||||
seal.sys.Wayland(&seal.sync, outerPath.String(), socketPath.String(), appID, seal.id.String())
|
k.sys.Wayland(&k.sync, outerPath.String(), socketPath.String(), appID, k.id.String())
|
||||||
seal.container.Bind(outerPath, innerPath, 0)
|
k.container.Bind(outerPath, innerPath, 0)
|
||||||
} else { // bind mount wayland socket (insecure)
|
} else { // bind mount wayland socket (insecure)
|
||||||
hlog.Verbose("direct wayland access, PROCEED WITH CAUTION")
|
hlog.Verbose("direct wayland access, PROCEED WITH CAUTION")
|
||||||
share.ensureRuntimeDir()
|
share.ensureRuntimeDir()
|
||||||
seal.container.Bind(socketPath, innerPath, 0)
|
k.container.Bind(socketPath, innerPath, 0)
|
||||||
seal.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute)
|
k.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Enablements.Unwrap()&system.EX11 != 0 {
|
if config.Enablements.Unwrap()&system.EX11 != 0 {
|
||||||
if d, ok := sys.LookupEnv(display); !ok {
|
if d, ok := k.lookupEnv(display); !ok {
|
||||||
return hlog.WrapErr(ErrXDisplay,
|
return newWithMessage("DISPLAY is not set")
|
||||||
"DISPLAY is not set")
|
|
||||||
} else {
|
} else {
|
||||||
socketDir := container.AbsFHSTmp.Append(".X11-unix")
|
socketDir := container.AbsFHSTmp.Append(".X11-unix")
|
||||||
|
|
||||||
@ -411,20 +405,21 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if socketPath != nil {
|
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) {
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
return hlog.WrapErrSuffix(err,
|
return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err}
|
||||||
fmt.Sprintf("cannot access X11 socket %q:", socketPath))
|
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
||||||
d = "unix:" + socketPath.String()
|
if !config.Container.HostAbstract {
|
||||||
|
d = "unix:" + socketPath.String()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
seal.sys.ChangeHosts("#" + seal.user.uid.String())
|
k.sys.ChangeHosts("#" + k.user.uid.String())
|
||||||
seal.env[display] = d
|
k.env[display] = d
|
||||||
seal.container.Bind(socketDir, socketDir, 0)
|
k.container.Bind(socketDir, socketDir, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,46 +429,101 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
|||||||
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
|
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
|
||||||
pulseSocket := pulseRuntimeDir.Append("native")
|
pulseSocket := pulseRuntimeDir.Append("native")
|
||||||
|
|
||||||
if _, err := sys.Stat(pulseRuntimeDir.String()); err != nil {
|
if _, err := k.stat(pulseRuntimeDir.String()); err != nil {
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
return hlog.WrapErrSuffix(err,
|
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err}
|
||||||
fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir))
|
|
||||||
}
|
}
|
||||||
return hlog.WrapErr(ErrPulseSocket,
|
return newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
|
||||||
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) {
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
return hlog.WrapErrSuffix(err,
|
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err}
|
||||||
fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket))
|
|
||||||
}
|
}
|
||||||
return hlog.WrapErr(ErrPulseSocket,
|
return newWithMessage(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
|
||||||
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
|
|
||||||
} else {
|
} else {
|
||||||
if m := s.Mode(); m&0o006 != 0o006 {
|
if m := s.Mode(); m&0o006 != 0o006 {
|
||||||
return hlog.WrapErr(ErrPulseMode,
|
return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m))
|
||||||
fmt.Sprintf("unexpected permissions on %q:", pulseSocket), m)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hard link pulse socket into target-executable share
|
// hard link pulse socket into target-executable share
|
||||||
innerPulseRuntimeDir := share.runtime().Append("pulse")
|
innerPulseRuntimeDir := share.runtime().Append("pulse")
|
||||||
innerPulseSocket := innerRuntimeDir.Append("pulse", "native")
|
innerPulseSocket := innerRuntimeDir.Append("pulse", "native")
|
||||||
seal.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String())
|
k.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String())
|
||||||
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
|
k.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
|
||||||
seal.env[pulseServer] = "unix:" + innerPulseSocket.String()
|
k.env[pulseServer] = "unix:" + innerPulseSocket.String()
|
||||||
|
|
||||||
// publish current user's pulse cookie for target user
|
// publish current user's pulse cookie for target user
|
||||||
if src, err := discoverPulseCookie(sys); err != nil {
|
var paCookiePath *container.Absolute
|
||||||
// not fatal
|
{
|
||||||
hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message()))
|
const paLocateStep = "locate PulseAudio cookie"
|
||||||
} else {
|
|
||||||
|
// 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")
|
innerDst := hst.AbsTmp.Append("/pulse-cookie")
|
||||||
seal.env[pulseCookie] = innerDst.String()
|
k.env[pulseCookie] = innerDst.String()
|
||||||
var payload *[]byte
|
var payload *[]byte
|
||||||
seal.container.PlaceP(innerDst, &payload)
|
k.container.PlaceP(innerDst, &payload)
|
||||||
seal.sys.CopyFile(payload, src, 256, 256)
|
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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -487,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")
|
sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket")
|
||||||
|
|
||||||
// configure dbus proxy
|
// configure dbus proxy
|
||||||
if f, err := seal.sys.ProxyDBus(
|
if f, err := k.sys.ProxyDBus(
|
||||||
config.SessionBus, config.SystemBus,
|
config.SessionBus, config.SystemBus,
|
||||||
sessionPath.String(), systemPath.String(),
|
sessionPath.String(), systemPath.String(),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
seal.dbusMsg = f
|
k.dbusMsg = f
|
||||||
}
|
}
|
||||||
|
|
||||||
// share proxy sockets
|
// share proxy sockets
|
||||||
sessionInner := innerRuntimeDir.Append("bus")
|
sessionInner := innerRuntimeDir.Append("bus")
|
||||||
seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String()
|
k.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String()
|
||||||
seal.container.Bind(sessionPath, sessionInner, 0)
|
k.container.Bind(sessionPath, sessionInner, 0)
|
||||||
seal.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write)
|
k.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write)
|
||||||
if config.SystemBus != nil {
|
if config.SystemBus != nil {
|
||||||
systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket")
|
systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket")
|
||||||
seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String()
|
k.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String()
|
||||||
seal.container.Bind(systemPath, systemInner, 0)
|
k.container.Bind(systemPath, systemInner, 0)
|
||||||
seal.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write)
|
k.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mount root read-only as the final setup Op
|
// mount root read-only as the final setup Op
|
||||||
seal.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY)
|
k.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY)
|
||||||
|
|
||||||
// append ExtraPerms last
|
// append ExtraPerms last
|
||||||
for _, p := range config.ExtraPerms {
|
for _, p := range config.ExtraPerms {
|
||||||
@ -519,7 +569,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
if p.Ensure {
|
if p.Ensure {
|
||||||
seal.sys.Ensure(p.Path.String(), 0700)
|
k.sys.Ensure(p.Path.String(), 0700)
|
||||||
}
|
}
|
||||||
|
|
||||||
perms := make(acl.Perms, 0, 3)
|
perms := make(acl.Perms, 0, 3)
|
||||||
@ -532,63 +582,24 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
|||||||
if p.Execute {
|
if p.Execute {
|
||||||
perms = append(perms, acl.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
|
// flatten and sort env for deterministic behaviour
|
||||||
seal.container.Env = make([]string, 0, len(seal.env))
|
k.container.Env = make([]string, 0, len(k.env))
|
||||||
for k, v := range seal.env {
|
for key, value := range k.env {
|
||||||
if strings.IndexByte(k, '=') != -1 {
|
if strings.IndexByte(key, '=') != -1 {
|
||||||
return hlog.WrapErr(syscall.EINVAL,
|
return &hst.AppError{Step: "flatten environment", Err: syscall.EINVAL,
|
||||||
fmt.Sprintf("invalid environment variable %s", k))
|
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() {
|
if hlog.Load() {
|
||||||
hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
|
hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
|
||||||
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
|
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))
|
|
||||||
}
|
|
||||||
96
internal/app/hsu.go
Normal file
96
internal/app/hsu.go
Normal file
@ -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 }
|
||||||
36
internal/app/paths.go
Normal file
36
internal/app/paths.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
339
internal/app/process.go
Normal file
339
internal/app/process.go
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/internal"
|
||||||
|
"hakurei.app/internal/app/state"
|
||||||
|
"hakurei.app/internal/hlog"
|
||||||
|
"hakurei.app/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
// duration to wait for shim to exit, after container WaitDelay has elapsed.
|
||||||
|
const shimWaitTimeout = 5 * time.Second
|
||||||
|
|
||||||
|
// mainState holds persistent state bound to outcome.main.
|
||||||
|
type mainState struct {
|
||||||
|
// done is whether beforeExit has been called already.
|
||||||
|
done bool
|
||||||
|
|
||||||
|
// Time is the exact point in time where the process was created.
|
||||||
|
// Location must be set to UTC.
|
||||||
|
//
|
||||||
|
// Time is nil if no process was ever created.
|
||||||
|
Time *time.Time
|
||||||
|
|
||||||
|
store state.Store
|
||||||
|
cancel context.CancelFunc
|
||||||
|
cmd *exec.Cmd
|
||||||
|
cmdWait chan error
|
||||||
|
|
||||||
|
k *outcome
|
||||||
|
uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// mainNeedsRevert indicates the call to Commit has succeeded.
|
||||||
|
mainNeedsRevert uintptr = 1 << iota
|
||||||
|
// mainNeedsDestroy indicates the instance state entry is present in the store.
|
||||||
|
mainNeedsDestroy
|
||||||
|
)
|
||||||
|
|
||||||
|
// beforeExit must be called immediately before a call to [os.Exit].
|
||||||
|
func (ms mainState) beforeExit(isFault bool) {
|
||||||
|
if ms.done {
|
||||||
|
panic("attempting to call beforeExit twice")
|
||||||
|
}
|
||||||
|
ms.done = true
|
||||||
|
defer hlog.BeforeExit()
|
||||||
|
|
||||||
|
if isFault && ms.cancel != nil {
|
||||||
|
ms.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasErr bool
|
||||||
|
// updates hasErr but does not terminate
|
||||||
|
perror := func(err error, message string) {
|
||||||
|
hasErr = true
|
||||||
|
printMessageError("cannot "+message+":", err)
|
||||||
|
}
|
||||||
|
exitCode := 1
|
||||||
|
defer func() {
|
||||||
|
if hasErr {
|
||||||
|
os.Exit(exitCode)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// this also handles wait for a non-fault termination
|
||||||
|
if ms.cmd != nil && ms.cmdWait != nil {
|
||||||
|
waitDone := make(chan struct{})
|
||||||
|
// TODO(ophestra): enforce this limit early so it does not have to be done twice
|
||||||
|
shimTimeoutCompensated := shimWaitTimeout
|
||||||
|
if ms.k.waitDelay > MaxShimWaitDelay {
|
||||||
|
shimTimeoutCompensated += MaxShimWaitDelay
|
||||||
|
} else {
|
||||||
|
shimTimeoutCompensated += ms.k.waitDelay
|
||||||
|
}
|
||||||
|
// this ties waitDone to ctx with the additional compensated timeout duration
|
||||||
|
go func() { <-ms.k.ctx.Done(); time.Sleep(shimTimeoutCompensated); close(waitDone) }()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-ms.cmdWait:
|
||||||
|
wstatus, ok := ms.cmd.ProcessState.Sys().(syscall.WaitStatus)
|
||||||
|
if ok {
|
||||||
|
if v := wstatus.ExitStatus(); v != 0 {
|
||||||
|
hasErr = true
|
||||||
|
exitCode = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hlog.Load() {
|
||||||
|
if !ok {
|
||||||
|
if err != nil {
|
||||||
|
hlog.Verbosef("wait: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch {
|
||||||
|
case wstatus.Exited():
|
||||||
|
hlog.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus())
|
||||||
|
|
||||||
|
case wstatus.CoreDump():
|
||||||
|
hlog.Verbosef("process %d dumped core", ms.cmd.Process.Pid)
|
||||||
|
|
||||||
|
case wstatus.Signaled():
|
||||||
|
hlog.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal())
|
||||||
|
|
||||||
|
default:
|
||||||
|
hlog.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-waitDone:
|
||||||
|
hlog.Resume()
|
||||||
|
// this is only reachable when shim did not exit within shimWaitTimeout, after its WaitDelay has elapsed.
|
||||||
|
// This is different from the container failing to terminate within its timeout period, as that is enforced
|
||||||
|
// by the shim. This path is instead reached when there is a lockup in shim preventing it from completing.
|
||||||
|
log.Printf("process %d did not terminate", ms.cmd.Process.Pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
hlog.Resume()
|
||||||
|
if ms.k.sync != nil {
|
||||||
|
if err := ms.k.sync.Close(); err != nil {
|
||||||
|
perror(err, "close wayland security context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ms.k.dbusMsg != nil {
|
||||||
|
ms.k.dbusMsg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ms.uintptr&mainNeedsRevert != 0 {
|
||||||
|
if ok, err := ms.store.Do(ms.k.user.identity.unwrap(), func(c state.Cursor) {
|
||||||
|
if ms.uintptr&mainNeedsDestroy != 0 {
|
||||||
|
if err := c.Destroy(ms.k.id.unwrap()); err != nil {
|
||||||
|
perror(err, "destroy state entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rt system.Enablement
|
||||||
|
if states, err := c.Load(); err != nil {
|
||||||
|
// it is impossible to continue from this point;
|
||||||
|
// revert per-process state here to limit damage
|
||||||
|
ec := system.Process
|
||||||
|
if revertErr := ms.k.sys.Revert((*system.Criteria)(&ec)); revertErr != nil {
|
||||||
|
var joinError interface {
|
||||||
|
Unwrap() []error
|
||||||
|
error
|
||||||
|
}
|
||||||
|
if !errors.As(revertErr, &joinError) || joinError == nil {
|
||||||
|
perror(revertErr, "revert system setup")
|
||||||
|
} else {
|
||||||
|
for _, v := range joinError.Unwrap() {
|
||||||
|
perror(v, "revert system setup step")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
perror(err, "load instance states")
|
||||||
|
} else {
|
||||||
|
ec := system.Process
|
||||||
|
if l := len(states); l == 0 {
|
||||||
|
ec |= system.User
|
||||||
|
} else {
|
||||||
|
hlog.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// accumulate enablements of remaining launchers
|
||||||
|
for i, s := range states {
|
||||||
|
if s.Config != nil {
|
||||||
|
rt |= s.Config.Enablements.Unwrap()
|
||||||
|
} else {
|
||||||
|
log.Printf("state entry %d does not contain config", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
|
||||||
|
if hlog.Load() {
|
||||||
|
if ec > 0 {
|
||||||
|
hlog.Verbose("reverting operations scope", system.TypeString(ec))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ms.k.sys.Revert((*system.Criteria)(&ec)); err != nil {
|
||||||
|
perror(err, "revert system setup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
if ok {
|
||||||
|
perror(err, "unlock state store")
|
||||||
|
} else {
|
||||||
|
perror(err, "open state store")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ms.uintptr&mainNeedsDestroy != 0 {
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ms.store != nil {
|
||||||
|
if err := ms.store.Close(); err != nil {
|
||||||
|
perror(err, "close state store")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fatal calls printMessageError, performs necessary cleanup, followed by a call to [os.Exit](1).
|
||||||
|
func (ms mainState) fatal(fallback string, ferr error) {
|
||||||
|
printMessageError(fallback, ferr)
|
||||||
|
ms.beforeExit(true)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// main carries out outcome and terminates. main does not return.
|
||||||
|
func (k *outcome) main() {
|
||||||
|
if !k.active.CompareAndSwap(false, true) {
|
||||||
|
panic("outcome: attempted to run twice")
|
||||||
|
}
|
||||||
|
|
||||||
|
// read comp value early for early failure
|
||||||
|
hsuPath := internal.MustHsuPath()
|
||||||
|
|
||||||
|
// ms.beforeExit required beyond this point
|
||||||
|
ms := &mainState{k: k}
|
||||||
|
|
||||||
|
if err := k.sys.Commit(); err != nil {
|
||||||
|
ms.fatal("cannot commit system setup:", err)
|
||||||
|
}
|
||||||
|
ms.uintptr |= mainNeedsRevert
|
||||||
|
ms.store = state.NewMulti(k.runDirPath.String())
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(k.ctx)
|
||||||
|
defer cancel()
|
||||||
|
ms.cancel = cancel
|
||||||
|
|
||||||
|
ms.cmd = exec.CommandContext(ctx, hsuPath)
|
||||||
|
ms.cmd.Stdin, ms.cmd.Stdout, ms.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
ms.cmd.Dir = container.FHSRoot // container init enters final working directory
|
||||||
|
// shim runs in the same session as monitor; see shim.go for behaviour
|
||||||
|
ms.cmd.Cancel = func() error { return ms.cmd.Process.Signal(syscall.SIGCONT) }
|
||||||
|
|
||||||
|
var e *gob.Encoder
|
||||||
|
if fd, encoder, err := container.Setup(&ms.cmd.ExtraFiles); err != nil {
|
||||||
|
ms.fatal("cannot create shim setup pipe:", err)
|
||||||
|
} else {
|
||||||
|
e = encoder
|
||||||
|
ms.cmd.Env = []string{
|
||||||
|
// passed through to shim by hsu
|
||||||
|
shimEnv + "=" + strconv.Itoa(fd),
|
||||||
|
// interpreted by hsu
|
||||||
|
"HAKUREI_IDENTITY=" + k.user.identity.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(k.user.supp) > 0 {
|
||||||
|
hlog.Verbosef("attaching supplementary group ids %s", k.user.supp)
|
||||||
|
// interpreted by hsu
|
||||||
|
ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(k.user.supp, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
hlog.Verbosef("setuid helper at %s", hsuPath)
|
||||||
|
hlog.Suspend()
|
||||||
|
if err := ms.cmd.Start(); err != nil {
|
||||||
|
ms.fatal("cannot start setuid wrapper:", err)
|
||||||
|
}
|
||||||
|
startTime := time.Now().UTC()
|
||||||
|
ms.cmdWait = make(chan error, 1)
|
||||||
|
// this ties context back to the life of the process
|
||||||
|
go func() { ms.cmdWait <- ms.cmd.Wait(); cancel() }()
|
||||||
|
ms.Time = &startTime
|
||||||
|
|
||||||
|
// unfortunately the I/O here cannot be directly canceled;
|
||||||
|
// the cancellation path leads to fatal in this case so that is fine
|
||||||
|
select {
|
||||||
|
case err := <-func() (setupErr chan error) {
|
||||||
|
setupErr = make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
setupErr <- e.Encode(&shimParams{
|
||||||
|
os.Getpid(),
|
||||||
|
k.waitDelay,
|
||||||
|
k.container,
|
||||||
|
hlog.Load(),
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}():
|
||||||
|
if err != nil {
|
||||||
|
hlog.Resume()
|
||||||
|
ms.fatal("cannot transmit shim config:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
hlog.Resume()
|
||||||
|
ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// shim accepted setup payload, create process state
|
||||||
|
if ok, err := ms.store.Do(k.user.identity.unwrap(), func(c state.Cursor) {
|
||||||
|
if err := c.Save(&state.State{
|
||||||
|
ID: k.id.unwrap(),
|
||||||
|
PID: ms.cmd.Process.Pid,
|
||||||
|
Time: *ms.Time,
|
||||||
|
}, k.ct); err != nil {
|
||||||
|
ms.fatal("cannot save state entry:", err)
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
if ok {
|
||||||
|
ms.uintptr |= mainNeedsDestroy
|
||||||
|
ms.fatal("cannot unlock state store:", err)
|
||||||
|
} else {
|
||||||
|
ms.fatal("cannot open state store:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// state in store at this point, destroy defunct state entry on termination
|
||||||
|
ms.uintptr |= mainNeedsDestroy
|
||||||
|
|
||||||
|
// beforeExit ties shim process to context
|
||||||
|
ms.beforeExit(false)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// printMessageError prints the error message according to [container.GetErrorMessage],
|
||||||
|
// or fallback prepended to err if an error message is not available.
|
||||||
|
func printMessageError(fallback string, err error) {
|
||||||
|
m, ok := container.GetErrorMessage(err)
|
||||||
|
if !ok {
|
||||||
|
log.Println(fallback, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Print(m)
|
||||||
|
}
|
||||||
@ -1,201 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/gob"
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"hakurei.app/container"
|
|
||||||
"hakurei.app/internal"
|
|
||||||
"hakurei.app/internal/app/state"
|
|
||||||
"hakurei.app/internal/hlog"
|
|
||||||
"hakurei.app/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
const shimWaitTimeout = 5 * time.Second
|
|
||||||
|
|
||||||
func (seal *outcome) Run(rs *RunState) error {
|
|
||||||
if !seal.f.CompareAndSwap(false, true) {
|
|
||||||
// run does much more than just starting a process; calling it twice, even if the first call fails, will result
|
|
||||||
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
|
|
||||||
// other Run a chance to return
|
|
||||||
return errors.New("outcome: attempted to run twice")
|
|
||||||
}
|
|
||||||
|
|
||||||
if rs == nil {
|
|
||||||
panic("invalid state")
|
|
||||||
}
|
|
||||||
|
|
||||||
// read comp value early to allow for early failure
|
|
||||||
hsuPath := internal.MustHsuPath()
|
|
||||||
|
|
||||||
if err := seal.sys.Commit(seal.ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
store := state.NewMulti(seal.runDirPath.String())
|
|
||||||
deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store
|
|
||||||
defer func() {
|
|
||||||
var revertErr error
|
|
||||||
storeErr := new(StateStoreError)
|
|
||||||
storeErr.Inner, storeErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
|
|
||||||
revertErr = func() error {
|
|
||||||
storeErr.InnerErr = deferredStoreFunc(c)
|
|
||||||
|
|
||||||
var rt system.Enablement
|
|
||||||
ec := system.Process
|
|
||||||
if states, err := c.Load(); err != nil {
|
|
||||||
// revert per-process state here to limit damage
|
|
||||||
storeErr.OpErr = err
|
|
||||||
return seal.sys.Revert((*system.Criteria)(&ec))
|
|
||||||
} else {
|
|
||||||
if l := len(states); l == 0 {
|
|
||||||
ec |= system.User
|
|
||||||
} else {
|
|
||||||
hlog.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
|
|
||||||
}
|
|
||||||
|
|
||||||
// accumulate enablements of remaining launchers
|
|
||||||
for i, s := range states {
|
|
||||||
if s.Config != nil {
|
|
||||||
rt |= s.Config.Enablements.Unwrap()
|
|
||||||
} else {
|
|
||||||
log.Printf("state entry %d does not contain config", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
|
|
||||||
if hlog.Load() {
|
|
||||||
if ec > 0 {
|
|
||||||
hlog.Verbose("reverting operations scope", system.TypeString(ec))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return seal.sys.Revert((*system.Criteria)(&ec))
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
storeErr.save(revertErr, store.Close())
|
|
||||||
rs.RevertErr = storeErr.equiv("error during cleanup:")
|
|
||||||
}()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(seal.ctx)
|
|
||||||
defer cancel()
|
|
||||||
cmd := exec.CommandContext(ctx, hsuPath)
|
|
||||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
||||||
cmd.Dir = container.FHSRoot // container init enters final working directory
|
|
||||||
// shim runs in the same session as monitor; see shim.go for behaviour
|
|
||||||
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
|
|
||||||
|
|
||||||
var e *gob.Encoder
|
|
||||||
if fd, encoder, err := container.Setup(&cmd.ExtraFiles); err != nil {
|
|
||||||
return hlog.WrapErrSuffix(err,
|
|
||||||
"cannot create shim setup pipe:")
|
|
||||||
} else {
|
|
||||||
e = encoder
|
|
||||||
cmd.Env = []string{
|
|
||||||
// passed through to shim by hsu
|
|
||||||
shimEnv + "=" + strconv.Itoa(fd),
|
|
||||||
// interpreted by hsu
|
|
||||||
"HAKUREI_APP_ID=" + seal.user.aid.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(seal.user.supp) > 0 {
|
|
||||||
hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp)
|
|
||||||
// interpreted by hsu
|
|
||||||
cmd.Env = append(cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " "))
|
|
||||||
}
|
|
||||||
|
|
||||||
hlog.Verbosef("setuid helper at %s", hsuPath)
|
|
||||||
hlog.Suspend()
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return hlog.WrapErrSuffix(err,
|
|
||||||
"cannot start setuid wrapper:")
|
|
||||||
}
|
|
||||||
rs.SetStart()
|
|
||||||
|
|
||||||
// this prevents blocking forever on an early failure
|
|
||||||
waitErr, setupErr := make(chan error, 1), make(chan error, 1)
|
|
||||||
go func() { waitErr <- cmd.Wait(); cancel() }()
|
|
||||||
go func() {
|
|
||||||
setupErr <- e.Encode(&shimParams{
|
|
||||||
os.Getpid(),
|
|
||||||
seal.waitDelay,
|
|
||||||
seal.container,
|
|
||||||
hlog.Load(),
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-setupErr:
|
|
||||||
if err != nil {
|
|
||||||
hlog.Resume()
|
|
||||||
return hlog.WrapErrSuffix(err,
|
|
||||||
"cannot transmit shim config:")
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-ctx.Done():
|
|
||||||
hlog.Resume()
|
|
||||||
return hlog.WrapErr(syscall.ECANCELED,
|
|
||||||
"shim setup canceled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// returned after blocking on waitErr
|
|
||||||
var earlyStoreErr = new(StateStoreError)
|
|
||||||
{
|
|
||||||
// shim accepted setup payload, create process state
|
|
||||||
sd := state.State{
|
|
||||||
ID: seal.id.unwrap(),
|
|
||||||
PID: cmd.Process.Pid,
|
|
||||||
Time: *rs.Time,
|
|
||||||
}
|
|
||||||
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
|
|
||||||
earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// state in store at this point, destroy defunct state entry on return
|
|
||||||
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
|
|
||||||
|
|
||||||
waitTimeout := make(chan struct{})
|
|
||||||
go func() { <-seal.ctx.Done(); time.Sleep(shimWaitTimeout); close(waitTimeout) }()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case rs.WaitErr = <-waitErr:
|
|
||||||
rs.WaitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus)
|
|
||||||
if hlog.Load() {
|
|
||||||
switch {
|
|
||||||
case rs.Exited():
|
|
||||||
hlog.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus())
|
|
||||||
case rs.CoreDump():
|
|
||||||
hlog.Verbosef("process %d dumped core", cmd.Process.Pid)
|
|
||||||
case rs.Signaled():
|
|
||||||
hlog.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal())
|
|
||||||
default:
|
|
||||||
hlog.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <-waitTimeout:
|
|
||||||
rs.WaitErr = syscall.ETIMEDOUT
|
|
||||||
hlog.Resume()
|
|
||||||
log.Printf("process %d did not terminate", cmd.Process.Pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
hlog.Resume()
|
|
||||||
if seal.sync != nil {
|
|
||||||
if err := seal.sync.Close(); err != nil {
|
|
||||||
log.Printf("cannot close wayland security context: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if seal.dbusMsg != nil {
|
|
||||||
seal.dbusMsg()
|
|
||||||
}
|
|
||||||
|
|
||||||
return earlyStoreErr.equiv("cannot save process state:")
|
|
||||||
}
|
|
||||||
@ -45,8 +45,10 @@ const (
|
|||||||
// ShimExitOrphan is returned when the shim is orphaned before monitor delivers a signal.
|
// ShimExitOrphan is returned when the shim is orphaned before monitor delivers a signal.
|
||||||
ShimExitOrphan = 3
|
ShimExitOrphan = 3
|
||||||
|
|
||||||
|
// DefaultShimWaitDelay is used when WaitDelay has its zero value.
|
||||||
DefaultShimWaitDelay = 5 * time.Second
|
DefaultShimWaitDelay = 5 * time.Second
|
||||||
MaxShimWaitDelay = 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.
|
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
|
||||||
@ -65,7 +67,7 @@ func ShimMain() {
|
|||||||
if errors.Is(err, syscall.EBADF) {
|
if errors.Is(err, syscall.EBADF) {
|
||||||
log.Fatal("invalid config descriptor")
|
log.Fatal("invalid config descriptor")
|
||||||
}
|
}
|
||||||
if errors.Is(err, container.ErrNotSet) {
|
if errors.Is(err, container.ErrReceiveEnv) {
|
||||||
log.Fatal("HAKUREI_SHIM not set")
|
log.Fatal("HAKUREI_SHIM not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,11 +157,11 @@ func ShimMain() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := z.Start(); err != nil {
|
if err := z.Start(); err != nil {
|
||||||
hlog.PrintBaseError(err, "cannot start container:")
|
printMessageError("cannot start container:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if err := z.Serve(); err != nil {
|
if err := z.Serve(); err != nil {
|
||||||
hlog.PrintBaseError(err, "cannot configure container:")
|
printMessageError("cannot configure container:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := seccomp.Load(
|
if err := seccomp.Load(
|
||||||
@ -27,27 +27,27 @@ type multiStore struct {
|
|||||||
lock sync.RWMutex
|
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()
|
s.lock.RLock()
|
||||||
defer s.lock.RUnlock()
|
defer s.lock.RUnlock()
|
||||||
|
|
||||||
// load or initialise new backend
|
// load or initialise new backend
|
||||||
b := new(multiBackend)
|
b := new(multiBackend)
|
||||||
b.lock.Lock()
|
b.lock.Lock()
|
||||||
if v, ok := s.backends.LoadOrStore(aid, b); ok {
|
if v, ok := s.backends.LoadOrStore(identity, b); ok {
|
||||||
b = v.(*multiBackend)
|
b = v.(*multiBackend)
|
||||||
} else {
|
} else {
|
||||||
b.path = path.Join(s.base, strconv.Itoa(aid))
|
b.path = path.Join(s.base, strconv.Itoa(identity))
|
||||||
|
|
||||||
// ensure directory
|
// ensure directory
|
||||||
if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||||
s.backends.CompareAndDelete(aid, b)
|
s.backends.CompareAndDelete(identity, b)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// open locker file
|
// open locker file
|
||||||
if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
|
if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
|
||||||
s.backends.CompareAndDelete(aid, b)
|
s.backends.CompareAndDelete(identity, b)
|
||||||
return false, err
|
return false, err
|
||||||
} else {
|
} else {
|
||||||
b.lockfile = l
|
b.lockfile = l
|
||||||
|
|||||||
@ -17,7 +17,7 @@ type Store interface {
|
|||||||
// Do calls f exactly once and ensures store exclusivity until f returns.
|
// Do calls f exactly once and ensures store exclusivity until f returns.
|
||||||
// Returns whether f is called and any errors during the locking process.
|
// Returns whether f is called and any errors during the locking process.
|
||||||
// Cursor provided to f becomes invalid as soon as f returns.
|
// Cursor provided to f becomes invalid as soon as f returns.
|
||||||
Do(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.
|
// List queries the store and returns a list of aids known to the store.
|
||||||
// Note that some or all returned aids might not have any active apps.
|
// Note that some or all returned aids might not have any active apps.
|
||||||
|
|||||||
@ -2,12 +2,9 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"hakurei.app/internal/app/state"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
|
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
|
||||||
func newID(id *state.ID) *stringPair[state.ID] { return &stringPair[state.ID]{*id, id.String()} }
|
|
||||||
|
|
||||||
// stringPair stores a value and its string representation.
|
// stringPair stores a value and its string representation.
|
||||||
type stringPair[T comparable] struct {
|
type stringPair[T comparable] struct {
|
||||||
|
|||||||
8
internal/app/sysconf.go
Normal file
8
internal/app/sysconf.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
//#include <unistd.h>
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
const _SC_LOGIN_NAME_MAX = C._SC_LOGIN_NAME_MAX
|
||||||
|
|
||||||
|
func sysconf(name C.int) int { return int(C.sysconf(name)) }
|
||||||
12
internal/app/username.go
Normal file
12
internal/app/username.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
// nameRegex is the default NAME_REGEX value from adduser.
|
||||||
|
var nameRegex = regexp.MustCompilePOSIX(`^[a-zA-Z][a-zA-Z0-9_-]*\$?$`)
|
||||||
|
|
||||||
|
// isValidUsername returns whether the argument is a valid username
|
||||||
|
func isValidUsername(username string) bool {
|
||||||
|
return len(username) < sysconf(_SC_LOGIN_NAME_MAX) &&
|
||||||
|
nameRegex.MatchString(username)
|
||||||
|
}
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -2,69 +2,17 @@
|
|||||||
package hlog
|
package hlog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
"hakurei.app/container"
|
||||||
"syscall"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var o = &container.Suspendable{Downstream: os.Stderr}
|
||||||
bufSize = 4 * 1024
|
|
||||||
bufSizeMax = 16 * 1024 * 1024
|
|
||||||
)
|
|
||||||
|
|
||||||
var o = &suspendable{w: os.Stderr}
|
|
||||||
|
|
||||||
// Prepare configures the system logger for [Suspend] and [Resume] to take effect.
|
// Prepare configures the system logger for [Suspend] and [Resume] to take effect.
|
||||||
func Prepare(prefix string) { log.SetPrefix(prefix + ": "); log.SetFlags(0); log.SetOutput(o) }
|
func Prepare(prefix string) { log.SetPrefix(prefix + ": "); log.SetFlags(0); log.SetOutput(o) }
|
||||||
|
|
||||||
type suspendable struct {
|
|
||||||
w io.Writer
|
|
||||||
s atomic.Bool
|
|
||||||
|
|
||||||
buf bytes.Buffer
|
|
||||||
bufOnce sync.Once
|
|
||||||
bufMu sync.Mutex
|
|
||||||
dropped int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *suspendable) Write(p []byte) (n int, err error) {
|
|
||||||
if !s.s.Load() {
|
|
||||||
return s.w.Write(p)
|
|
||||||
}
|
|
||||||
s.bufOnce.Do(func() { s.prepareBuf() })
|
|
||||||
|
|
||||||
s.bufMu.Lock()
|
|
||||||
defer s.bufMu.Unlock()
|
|
||||||
|
|
||||||
if l := len(p); s.buf.Len()+l > bufSizeMax {
|
|
||||||
s.dropped += l
|
|
||||||
return 0, syscall.ENOMEM
|
|
||||||
}
|
|
||||||
return s.buf.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *suspendable) prepareBuf() { s.buf.Grow(bufSize) }
|
|
||||||
func (s *suspendable) Suspend() bool { return o.s.CompareAndSwap(false, true) }
|
|
||||||
func (s *suspendable) Resume() (resumed bool, dropped uintptr, n int64, err error) {
|
|
||||||
if o.s.CompareAndSwap(true, false) {
|
|
||||||
o.bufMu.Lock()
|
|
||||||
defer o.bufMu.Unlock()
|
|
||||||
|
|
||||||
resumed = true
|
|
||||||
dropped = uintptr(o.dropped)
|
|
||||||
|
|
||||||
o.dropped = 0
|
|
||||||
n, err = io.Copy(s.w, &s.buf)
|
|
||||||
s.buf = bytes.Buffer{}
|
|
||||||
s.prepareBuf()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func Suspend() bool { return o.Suspend() }
|
func Suspend() bool { return o.Suspend() }
|
||||||
func Resume() bool {
|
func Resume() bool {
|
||||||
resumed, dropped, _, err := o.Resume()
|
resumed, dropped, _, err := o.Resume()
|
||||||
|
|||||||
@ -2,11 +2,9 @@ package hlog
|
|||||||
|
|
||||||
type Output struct{}
|
type Output struct{}
|
||||||
|
|
||||||
func (Output) IsVerbose() bool { return Load() }
|
func (Output) IsVerbose() bool { return Load() }
|
||||||
func (Output) Verbose(v ...any) { Verbose(v...) }
|
func (Output) Verbose(v ...any) { Verbose(v...) }
|
||||||
func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) }
|
func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) }
|
||||||
func (Output) WrapErr(err error, a ...any) error { return WrapErr(err, a...) }
|
func (Output) Suspend() { Suspend() }
|
||||||
func (Output) PrintBaseErr(err error, fallback string) { PrintBaseError(err, fallback) }
|
func (Output) Resume() bool { return Resume() }
|
||||||
func (Output) Suspend() { Suspend() }
|
func (Output) BeforeExit() { BeforeExit() }
|
||||||
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
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
//#include <unistd.h>
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
const SC_LOGIN_NAME_MAX = C._SC_LOGIN_NAME_MAX
|
|
||||||
|
|
||||||
func Sysconf(name C.int) int { return int(C.sysconf(name)) }
|
|
||||||
@ -35,7 +35,7 @@ package
|
|||||||
|
|
||||||
|
|
||||||
*Default:*
|
*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:*
|
*Type:*
|
||||||
anything
|
list of attribute set of anything
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -723,7 +723,7 @@ Common extra paths to make available to the container\.
|
|||||||
|
|
||||||
|
|
||||||
*Type:*
|
*Type:*
|
||||||
anything
|
list of attribute set of anything
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -759,7 +759,7 @@ package
|
|||||||
|
|
||||||
|
|
||||||
*Default:*
|
*Default:*
|
||||||
` <derivation hakurei-hsu-0.2.1> `
|
` <derivation hakurei-hsu-0.2.2> `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -203,7 +203,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
extraPaths = mkOption {
|
extraPaths = mkOption {
|
||||||
type = anything;
|
type = listOf (attrsOf anything);
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
Extra paths to make available to the container.
|
Extra paths to make available to the container.
|
||||||
@ -261,7 +261,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
commonPaths = mkOption {
|
commonPaths = mkOption {
|
||||||
type = types.anything;
|
type = types.listOf (types.attrsOf types.anything);
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
Common extra paths to make available to the container.
|
Common extra paths to make available to the container.
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
buildGoModule rec {
|
buildGoModule rec {
|
||||||
pname = "hakurei";
|
pname = "hakurei";
|
||||||
version = "0.2.1";
|
version = "0.2.2";
|
||||||
|
|
||||||
srcFiltered = builtins.path {
|
srcFiltered = builtins.path {
|
||||||
name = "${pname}-src";
|
name = "${pname}-src";
|
||||||
|
|||||||
@ -9,65 +9,59 @@ import (
|
|||||||
"hakurei.app/system/acl"
|
"hakurei.app/system/acl"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdatePerm appends an ephemeral acl update Op.
|
// UpdatePerm calls UpdatePermType with the [Process] criteria.
|
||||||
func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I {
|
func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I {
|
||||||
sys.UpdatePermType(Process, path, perms...)
|
sys.UpdatePermType(Process, path, perms...)
|
||||||
|
|
||||||
return sys
|
return sys
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePermType appends an acl update Op.
|
// 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 {
|
func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I {
|
||||||
sys.lock.Lock()
|
sys.ops = append(sys.ops, &aclUpdateOp{et, path, perms})
|
||||||
defer sys.lock.Unlock()
|
|
||||||
|
|
||||||
sys.ops = append(sys.ops, &ACL{et, path, perms})
|
|
||||||
|
|
||||||
return sys
|
return sys
|
||||||
}
|
}
|
||||||
|
|
||||||
type ACL struct {
|
// aclUpdateOp implements [I.UpdatePermType].
|
||||||
|
type aclUpdateOp struct {
|
||||||
et Enablement
|
et Enablement
|
||||||
path string
|
path string
|
||||||
perms acl.Perms
|
perms acl.Perms
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ACL) Type() Enablement { return a.et }
|
func (a *aclUpdateOp) Type() Enablement { return a.et }
|
||||||
|
|
||||||
func (a *ACL) apply(sys *I) error {
|
func (a *aclUpdateOp) apply(sys *I) error {
|
||||||
msg.Verbose("applying ACL", a)
|
sys.verbose("applying ACL", a)
|
||||||
return wrapErrSuffix(acl.Update(a.path, sys.uid, a.perms...),
|
return newOpError("acl", sys.aclUpdate(a.path, sys.uid, a.perms...), false)
|
||||||
fmt.Sprintf("cannot apply ACL entry to %q:", a.path))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ACL) revert(sys *I, ec *Criteria) error {
|
func (a *aclUpdateOp) revert(sys *I, ec *Criteria) error {
|
||||||
if ec.hasType(a) {
|
if ec.hasType(a.Type()) {
|
||||||
msg.Verbose("stripping ACL", a)
|
sys.verbose("stripping ACL", a)
|
||||||
err := acl.Update(a.path, sys.uid)
|
err := sys.aclUpdate(a.path, sys.uid)
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
// the ACL is effectively stripped if the file no longer exists
|
// the ACL is effectively stripped if the file no longer exists
|
||||||
msg.Verbosef("target of ACL %s no longer exists", a)
|
sys.verbosef("target of ACL %s no longer exists", a)
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
return wrapErrSuffix(err,
|
return newOpError("acl", err, true)
|
||||||
fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
|
|
||||||
} else {
|
} else {
|
||||||
msg.Verbose("skipping ACL", a)
|
sys.verbose("skipping ACL", a)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ACL) Is(o Op) bool {
|
func (a *aclUpdateOp) Is(o Op) bool {
|
||||||
a0, ok := o.(*ACL)
|
target, ok := o.(*aclUpdateOp)
|
||||||
return ok && a0 != nil &&
|
return ok && a != nil && target != nil &&
|
||||||
a.et == a0.et &&
|
a.et == target.et &&
|
||||||
a.path == a0.path &&
|
a.path == target.path &&
|
||||||
slices.Equal(a.perms, a0.perms)
|
slices.Equal(a.perms, target.perms)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ACL) Path() string { return a.path }
|
func (a *aclUpdateOp) Path() string { return a.path }
|
||||||
|
|
||||||
func (a *ACL) String() string {
|
func (a *aclUpdateOp) String() string {
|
||||||
return fmt.Sprintf("%s type: %s path: %q",
|
return fmt.Sprintf("%s type: %s path: %q",
|
||||||
a.perms, TypeString(a.et), a.path)
|
a.perms, TypeString(a.et), a.path)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,8 +29,5 @@ func Update(name string, uid int, perms ...Perm) error {
|
|||||||
(*C.acl_perm_t)(p),
|
(*C.acl_perm_t)(p),
|
||||||
C.size_t(len(perms)),
|
C.size_t(len(perms)),
|
||||||
)
|
)
|
||||||
if r == 0 {
|
return newAclPathError(name, int(r), err)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,156 +0,0 @@
|
|||||||
package acl_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
getFAclInvocation struct {
|
|
||||||
cmd *exec.Cmd
|
|
||||||
val []*getFAclResp
|
|
||||||
pe []error
|
|
||||||
}
|
|
||||||
|
|
||||||
getFAclResp struct {
|
|
||||||
typ fAclType
|
|
||||||
cred int32
|
|
||||||
val fAclPerm
|
|
||||||
|
|
||||||
raw []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
fAclPerm uintptr
|
|
||||||
fAclType uint8
|
|
||||||
)
|
|
||||||
|
|
||||||
const fAclBufSize = 16
|
|
||||||
|
|
||||||
const (
|
|
||||||
fAclPermRead fAclPerm = 1 << iota
|
|
||||||
fAclPermWrite
|
|
||||||
fAclPermExecute
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
fAclTypeUser fAclType = iota
|
|
||||||
fAclTypeGroup
|
|
||||||
fAclTypeMask
|
|
||||||
fAclTypeOther
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *getFAclInvocation) run(name string) error {
|
|
||||||
if c.cmd != nil {
|
|
||||||
panic("attempted to run twice")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name)
|
|
||||||
|
|
||||||
scanErr := make(chan error, 1)
|
|
||||||
if p, err := c.cmd.StdoutPipe(); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
go c.parse(p, scanErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.cmd.Start(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Join(<-scanErr, c.cmd.Wait())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
|
|
||||||
c.val = make([]*getFAclResp, 0, 4+fAclBufSize)
|
|
||||||
|
|
||||||
s := bufio.NewScanner(pipe)
|
|
||||||
for s.Scan() {
|
|
||||||
fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3)
|
|
||||||
if len(fields) != 3 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := getFAclResp{}
|
|
||||||
|
|
||||||
switch string(fields[0]) {
|
|
||||||
case "user":
|
|
||||||
resp.typ = fAclTypeUser
|
|
||||||
case "group":
|
|
||||||
resp.typ = fAclTypeGroup
|
|
||||||
case "mask":
|
|
||||||
resp.typ = fAclTypeMask
|
|
||||||
case "other":
|
|
||||||
resp.typ = fAclTypeOther
|
|
||||||
default:
|
|
||||||
c.pe = append(c.pe, fmt.Errorf("unknown type %s", string(fields[0])))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fields[1]) == 0 {
|
|
||||||
resp.cred = -1
|
|
||||||
} else {
|
|
||||||
if cred, err := strconv.Atoi(string(fields[1])); err != nil {
|
|
||||||
c.pe = append(c.pe, err)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
resp.cred = int32(cred)
|
|
||||||
if resp.cred < 0 {
|
|
||||||
c.pe = append(c.pe, fmt.Errorf("credential %d out of range", resp.cred))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fields[2]) != 3 {
|
|
||||||
c.pe = append(c.pe, fmt.Errorf("invalid perm length %d", len(fields[2])))
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
switch fields[2][0] {
|
|
||||||
case 'r':
|
|
||||||
resp.val |= fAclPermRead
|
|
||||||
case '-':
|
|
||||||
default:
|
|
||||||
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0]))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch fields[2][1] {
|
|
||||||
case 'w':
|
|
||||||
resp.val |= fAclPermWrite
|
|
||||||
case '-':
|
|
||||||
default:
|
|
||||||
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1]))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch fields[2][2] {
|
|
||||||
case 'x':
|
|
||||||
resp.val |= fAclPermExecute
|
|
||||||
case '-':
|
|
||||||
default:
|
|
||||||
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2]))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.raw = make([]byte, len(s.Bytes()))
|
|
||||||
copy(resp.raw, s.Bytes())
|
|
||||||
c.val = append(c.val, &resp)
|
|
||||||
}
|
|
||||||
scanErr <- s.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *getFAclResp) String() string {
|
|
||||||
if r.raw != nil && len(r.raw) > 0 {
|
|
||||||
return string(r.raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "(user-initialised resp value)"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool {
|
|
||||||
return r.typ == typ && r.cred == cred && r.val == val
|
|
||||||
}
|
|
||||||
@ -1,10 +1,16 @@
|
|||||||
package acl_test
|
package acl_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/system/acl"
|
"hakurei.app/system/acl"
|
||||||
@ -17,7 +23,7 @@ var (
|
|||||||
cred = int32(os.Geteuid())
|
cred = int32(os.Geteuid())
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUpdatePerm(t *testing.T) {
|
func TestUpdate(t *testing.T) {
|
||||||
if os.Getenv("GO_TEST_SKIP_ACL") == "1" {
|
if os.Getenv("GO_TEST_SKIP_ACL") == "1" {
|
||||||
t.Log("acl test skipped")
|
t.Log("acl test skipped")
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
@ -48,19 +54,19 @@ func TestUpdatePerm(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("default clear mask", func(t *testing.T) {
|
t.Run("default clear mask", func(t *testing.T) {
|
||||||
if err := acl.Update(testFilePath, uid); err != nil {
|
if err := acl.Update(testFilePath, uid); err != nil {
|
||||||
t.Fatalf("UpdatePerm: error = %v", err)
|
t.Fatalf("Update: error = %v", err)
|
||||||
}
|
}
|
||||||
if cur = getfacl(t, testFilePath); len(cur) != 4 {
|
if cur = getfacl(t, testFilePath); len(cur) != 4 {
|
||||||
t.Fatalf("UpdatePerm: %v", cur)
|
t.Fatalf("Update: %v", cur)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("default clear consistency", func(t *testing.T) {
|
t.Run("default clear consistency", func(t *testing.T) {
|
||||||
if err := acl.Update(testFilePath, uid); err != nil {
|
if err := acl.Update(testFilePath, uid); err != nil {
|
||||||
t.Fatalf("UpdatePerm: error = %v", err)
|
t.Fatalf("Update: error = %v", err)
|
||||||
}
|
}
|
||||||
if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) {
|
if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) {
|
||||||
t.Fatalf("UpdatePerm: %v, want %v", val, cur)
|
t.Fatalf("Update: %v, want %v", val, cur)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -77,26 +83,171 @@ func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
if err := acl.Update(testFilePath, uid); err != nil {
|
if err := acl.Update(testFilePath, uid); err != nil {
|
||||||
t.Fatalf("UpdatePerm: error = %v", err)
|
t.Fatalf("Update: error = %v", err)
|
||||||
}
|
}
|
||||||
if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) {
|
if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) {
|
||||||
t.Fatalf("UpdatePerm: %v, want %v", v, cur)
|
t.Fatalf("Update: %v, want %v", v, cur)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := acl.Update(testFilePath, uid, perms...); err != nil {
|
if err := acl.Update(testFilePath, uid, perms...); err != nil {
|
||||||
t.Fatalf("UpdatePerm: error = %v", err)
|
t.Fatalf("Update: error = %v", err)
|
||||||
}
|
}
|
||||||
r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred)
|
r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred)
|
||||||
if r == nil {
|
if r == nil {
|
||||||
t.Fatalf("UpdatePerm did not add an ACL entry")
|
t.Fatalf("Update did not add an ACL entry")
|
||||||
}
|
}
|
||||||
if !r.equals(fAclTypeUser, cred, val) {
|
if !r.equals(fAclTypeUser, cred, val) {
|
||||||
t.Fatalf("UpdatePerm(%s) = %s", name, r)
|
t.Fatalf("Update(%s) = %s", name, r)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
getFAclInvocation struct {
|
||||||
|
cmd *exec.Cmd
|
||||||
|
val []*getFAclResp
|
||||||
|
pe []error
|
||||||
|
}
|
||||||
|
|
||||||
|
getFAclResp struct {
|
||||||
|
typ fAclType
|
||||||
|
cred int32
|
||||||
|
val fAclPerm
|
||||||
|
|
||||||
|
raw []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
fAclPerm uintptr
|
||||||
|
fAclType uint8
|
||||||
|
)
|
||||||
|
|
||||||
|
const fAclBufSize = 16
|
||||||
|
|
||||||
|
const (
|
||||||
|
fAclPermRead fAclPerm = 1 << iota
|
||||||
|
fAclPermWrite
|
||||||
|
fAclPermExecute
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fAclTypeUser fAclType = iota
|
||||||
|
fAclTypeGroup
|
||||||
|
fAclTypeMask
|
||||||
|
fAclTypeOther
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *getFAclInvocation) run(name string) error {
|
||||||
|
if c.cmd != nil {
|
||||||
|
panic("attempted to run twice")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name)
|
||||||
|
|
||||||
|
scanErr := make(chan error, 1)
|
||||||
|
if p, err := c.cmd.StdoutPipe(); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
go c.parse(p, scanErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(<-scanErr, c.cmd.Wait())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
|
||||||
|
c.val = make([]*getFAclResp, 0, 4+fAclBufSize)
|
||||||
|
|
||||||
|
s := bufio.NewScanner(pipe)
|
||||||
|
for s.Scan() {
|
||||||
|
fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3)
|
||||||
|
if len(fields) != 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := getFAclResp{}
|
||||||
|
|
||||||
|
switch string(fields[0]) {
|
||||||
|
case "user":
|
||||||
|
resp.typ = fAclTypeUser
|
||||||
|
case "group":
|
||||||
|
resp.typ = fAclTypeGroup
|
||||||
|
case "mask":
|
||||||
|
resp.typ = fAclTypeMask
|
||||||
|
case "other":
|
||||||
|
resp.typ = fAclTypeOther
|
||||||
|
default:
|
||||||
|
c.pe = append(c.pe, fmt.Errorf("unknown type %s", string(fields[0])))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fields[1]) == 0 {
|
||||||
|
resp.cred = -1
|
||||||
|
} else {
|
||||||
|
if cred, err := strconv.Atoi(string(fields[1])); err != nil {
|
||||||
|
c.pe = append(c.pe, err)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
resp.cred = int32(cred)
|
||||||
|
if resp.cred < 0 {
|
||||||
|
c.pe = append(c.pe, fmt.Errorf("credential %d out of range", resp.cred))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fields[2]) != 3 {
|
||||||
|
c.pe = append(c.pe, fmt.Errorf("invalid perm length %d", len(fields[2])))
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
switch fields[2][0] {
|
||||||
|
case 'r':
|
||||||
|
resp.val |= fAclPermRead
|
||||||
|
case '-':
|
||||||
|
default:
|
||||||
|
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0]))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch fields[2][1] {
|
||||||
|
case 'w':
|
||||||
|
resp.val |= fAclPermWrite
|
||||||
|
case '-':
|
||||||
|
default:
|
||||||
|
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1]))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch fields[2][2] {
|
||||||
|
case 'x':
|
||||||
|
resp.val |= fAclPermExecute
|
||||||
|
case '-':
|
||||||
|
default:
|
||||||
|
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2]))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.raw = make([]byte, len(s.Bytes()))
|
||||||
|
copy(resp.raw, s.Bytes())
|
||||||
|
c.val = append(c.val, &resp)
|
||||||
|
}
|
||||||
|
scanErr <- s.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *getFAclResp) String() string {
|
||||||
|
if r.raw != nil && len(r.raw) > 0 {
|
||||||
|
return string(r.raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "(user-initialised resp value)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool {
|
||||||
|
return r.typ == typ && r.cred == cred && r.val == val
|
||||||
|
}
|
||||||
|
|
||||||
func getfacl(t *testing.T, name string) []*getFAclResp {
|
func getfacl(t *testing.T, name string) []*getFAclResp {
|
||||||
c := new(getFAclInvocation)
|
c := new(getFAclInvocation)
|
||||||
if err := c.run(name); err != nil {
|
if err := c.run(name); err != nil {
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
|
int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
|
||||||
acl_perm_t *perms, size_t plen) {
|
acl_perm_t *perms, size_t plen) {
|
||||||
int ret = -1;
|
int ret;
|
||||||
bool v;
|
bool v;
|
||||||
int i;
|
int i;
|
||||||
acl_t acl;
|
acl_t acl;
|
||||||
@ -15,51 +15,70 @@ int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
|
|||||||
void *qualifier_p;
|
void *qualifier_p;
|
||||||
acl_permset_t permset;
|
acl_permset_t permset;
|
||||||
|
|
||||||
|
ret = -1; /* acl_get_file */
|
||||||
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
|
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
|
||||||
if (acl == NULL)
|
if (acl == NULL)
|
||||||
goto out;
|
goto out;
|
||||||
|
|
||||||
// prune entries by uid
|
/* prune entries by uid */
|
||||||
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
|
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
|
||||||
i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
|
i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
|
||||||
|
ret = -2; /* acl_get_tag_type */
|
||||||
if (acl_get_tag_type(entry, &tag_type) != 0)
|
if (acl_get_tag_type(entry, &tag_type) != 0)
|
||||||
return -1;
|
goto out;
|
||||||
if (tag_type != ACL_USER)
|
if (tag_type != ACL_USER)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
ret = -3; /* acl_get_qualifier */
|
||||||
qualifier_p = acl_get_qualifier(entry);
|
qualifier_p = acl_get_qualifier(entry);
|
||||||
if (qualifier_p == NULL)
|
if (qualifier_p == NULL)
|
||||||
return -1;
|
goto out;
|
||||||
v = *(uid_t *)qualifier_p == uid;
|
v = *(uid_t *)qualifier_p == uid;
|
||||||
acl_free(qualifier_p);
|
acl_free(qualifier_p);
|
||||||
|
|
||||||
if (!v)
|
if (!v)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
acl_delete_entry(acl, entry);
|
ret = -4; /* acl_delete_entry */
|
||||||
|
if (acl_delete_entry(acl, entry) != 0)
|
||||||
|
goto out;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plen == 0)
|
if (plen == 0)
|
||||||
goto set;
|
goto set;
|
||||||
|
|
||||||
|
ret = -5; /* acl_create_entry */
|
||||||
if (acl_create_entry(&acl, &entry) != 0)
|
if (acl_create_entry(&acl, &entry) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
|
|
||||||
|
ret = -6; /* acl_get_permset */
|
||||||
if (acl_get_permset(entry, &permset) != 0)
|
if (acl_get_permset(entry, &permset) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
|
|
||||||
|
ret = -7; /* acl_add_perm */
|
||||||
for (i = 0; i < plen; i++) {
|
for (i = 0; i < plen; i++) {
|
||||||
if (acl_add_perm(permset, perms[i]) != 0)
|
if (acl_add_perm(permset, perms[i]) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ret = -8; /* acl_set_tag_type */
|
||||||
if (acl_set_tag_type(entry, ACL_USER) != 0)
|
if (acl_set_tag_type(entry, ACL_USER) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
|
|
||||||
|
ret = -9; /* acl_set_qualifier */
|
||||||
if (acl_set_qualifier(entry, (void *)&uid) != 0)
|
if (acl_set_qualifier(entry, (void *)&uid) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
|
|
||||||
set:
|
set:
|
||||||
|
ret = -10; /* acl_calc_mask */
|
||||||
if (acl_calc_mask(&acl) != 0)
|
if (acl_calc_mask(&acl) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
|
|
||||||
|
ret = -11; /* acl_valid */
|
||||||
if (acl_valid(acl) != 0)
|
if (acl_valid(acl) != 0)
|
||||||
goto out;
|
goto out;
|
||||||
|
|
||||||
|
ret = -12; /* acl_set_file */
|
||||||
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
|
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
|
||||||
ret = 0;
|
ret = 0;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user