Compare commits

..

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

164 changed files with 4008 additions and 5315 deletions

View File

@ -2,13 +2,10 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/user"
"strconv"
"sync"
@ -16,52 +13,36 @@ import (
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
func buildCommand(ctx context.Context, out io.Writer) command.Command {
var (
flagVerbose bool
flagJSON bool
)
c := command.New(out, log.Printf, "hakurei", func([]string) error {
msg.SwapVerbose(flagVerbose)
if early.yamaLSM != nil {
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", early.yamaLSM)
// not fatal
}
if early.dumpable != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", early.dumpable)
// not fatal
}
return nil
}).
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
c.Command("app", "Load and start container from configuration file", func(args []string) error {
c.Command("app", "Load app from configuration file", func(args []string) error {
if len(args) < 1 {
log.Fatal("app requires at least 1 argument")
}
// config extraArgs...
config := tryPath(msg, args[0])
if config != nil && config.Container != nil {
config.Container.Args = append(config.Container.Args, args[1:]...)
}
config := tryPath(args[0])
config.Args = append(config.Args, args[1:]...)
app.Main(ctx, msg, config)
app.Main(ctx, config)
panic("unreachable")
})
@ -81,8 +62,14 @@ func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningE
flagWayland, flagX11, flagDBus, flagPulse bool
)
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error {
if flagIdentity < hst.IdentityMin || flagIdentity > hst.IdentityMax {
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
// initialise config from flags
config := &hst.Config{
ID: flagID,
Args: args,
}
if flagIdentity < 0 || flagIdentity > 9999 {
log.Fatalf("identity %d out of range", flagIdentity)
}
@ -91,15 +78,15 @@ func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningE
passwd *user.User
passwdOnce sync.Once
passwdFunc = func() {
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustIDMsg(msg), flagIdentity))
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustID(), flagIdentity))
if u, err := user.LookupId(us); err != nil {
msg.Verbosef("cannot look up uid %s", us)
hlog.Verbosef("cannot look up uid %s", us)
passwd = &user.User{
Uid: us,
Gid: us,
Username: "chronos",
Name: "Hakurei Permissive Default",
HomeDir: fhs.VarEmpty,
HomeDir: container.FHSVarEmpty,
}
} else {
passwd = u
@ -107,128 +94,60 @@ func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningE
}
)
// paths are identical, resolve inner shell and program path
shell := fhs.AbsRoot.Append("bin", "sh")
if a, err := check.NewAbs(os.Getenv("SHELL")); err == nil {
shell = a
}
progPath := shell
if len(args) > 0 {
if p, err := exec.LookPath(args[0]); err != nil {
log.Fatal(errors.Unwrap(err))
return err
} else if progPath, err = check.NewAbs(p); err != nil {
log.Fatal(err.Error())
return err
}
}
var et hst.Enablement
if flagWayland {
et |= hst.EWayland
}
if flagX11 {
et |= hst.EX11
}
if flagDBus {
et |= hst.EDBus
}
if flagPulse {
et |= hst.EPulse
}
config := &hst.Config{
ID: flagID,
Identity: flagIdentity,
Groups: flagGroups,
Enablements: hst.NewEnablements(et),
Container: &hst.ContainerConfig{
Userns: true,
HostNet: true,
Tty: true,
HostAbstract: true,
Filesystem: []hst.FilesystemConfigJSON{
// autoroot, includes the home directory
{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsRoot,
Source: fhs.AbsRoot,
Write: true,
Special: true,
}},
},
Username: flagUserName,
Shell: shell,
Path: progPath,
Args: args,
},
}
// bind GPU stuff
if et&(hst.EX11|hst.EWayland) != 0 {
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("dri"),
Device: true,
Optional: true,
}})
}
config.Container.Filesystem = append(config.Container.Filesystem,
// opportunistically bind kvm
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("kvm"),
Device: true,
Optional: true,
}},
// do autoetc last
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsEtc,
Source: fhs.AbsEtc,
Special: true,
}},
)
if config.Container.Username == "chronos" {
if flagHomeDir == "os" {
passwdOnce.Do(passwdFunc)
config.Container.Username = passwd.Username
flagHomeDir = passwd.HomeDir
}
{
homeDir := flagHomeDir
if homeDir == "os" {
if flagUserName == "chronos" {
passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir
flagUserName = passwd.Username
}
if a, err := check.NewAbs(homeDir); err != nil {
config.Identity = flagIdentity
config.Groups = flagGroups
config.Username = flagUserName
if a, err := container.NewAbs(flagHomeDir); err != nil {
log.Fatal(err.Error())
return err
} else {
config.Container.Home = a
config.Home = a
}
var e system.Enablement
if flagWayland {
e |= system.EWayland
}
if flagX11 {
e |= system.EX11
}
if flagDBus {
e |= system.EDBus
}
if flagPulse {
e |= system.EPulse
}
config.Enablements = hst.NewEnablements(e)
// parse D-Bus config file from flags if applicable
if flagDBus {
if flagDBusConfigSession == "builtin" {
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
} else {
if f, err := os.Open(flagDBusConfigSession); err != nil {
log.Fatal(err.Error())
} else if err = json.NewDecoder(f).Decode(&config.SessionBus); err != nil {
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSession); err != nil {
log.Fatalf("cannot load session bus proxy config from %q: %s", flagDBusConfigSession, err)
} else {
config.SessionBus = conf
}
}
// system bus proxy is optional
if flagDBusConfigSystem != "nil" {
if f, err := os.Open(flagDBusConfigSystem); err != nil {
log.Fatal(err.Error())
} else if err = json.NewDecoder(f).Decode(&config.SystemBus); err != nil {
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSystem); err != nil {
log.Fatalf("cannot load system bus proxy config from %q: %s", flagDBusConfigSystem, err)
} else {
config.SystemBus = conf
}
}
@ -243,7 +162,7 @@ func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningE
}
}
app.Main(ctx, msg, config)
app.Main(ctx, config)
panic("unreachable")
}).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
@ -283,13 +202,11 @@ func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningE
case 1: // instance
name := args[0]
config, entry := tryShort(msg, name)
config, entry := tryShort(name)
if config == nil {
config = tryPath(msg, name)
}
if !printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON) {
os.Exit(1)
config = tryPath(name)
}
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON)
default:
log.Fatal("show requires 1 argument")
@ -302,16 +219,31 @@ func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningE
var flagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error {
var sc hst.Paths
app.CopyPaths().Copy(&sc, new(app.Hsu).MustID())
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(msg, sc.RunDirPath.String()), flagShort, flagJSON)
app.CopyPaths(&sc, new(app.Hsu).MustID())
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sc.RunDirPath.String()), flagShort, flagJSON)
return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
}
c.Command("version", "Display version information", func(args []string) error { fmt.Println(internal.Version()); return errSuccess })
c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
c.Command("template", "Produce a config template", func(args []string) error { printJSON(os.Stdout, false, hst.Template()); return errSuccess })
c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })
c.Command("version", "Display version information", func(args []string) error {
fmt.Println(internal.Version())
return errSuccess
})
c.Command("license", "Show full license text", func(args []string) error {
fmt.Println(license)
return errSuccess
})
c.Command("template", "Produce a config template", func(args []string) error {
printJSON(os.Stdout, false, hst.Template())
return errSuccess
})
c.Command("help", "Show this help message", func([]string) error {
c.PrintHelp()
return errSuccess
})
return c
}

View File

@ -7,7 +7,6 @@ import (
"testing"
"hakurei.app/command"
"hakurei.app/container"
)
func TestHelp(t *testing.T) {
@ -21,8 +20,8 @@ func TestHelp(t *testing.T) {
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Commands:
app Load and start container from configuration file
run Configure and start a permissive container
app Load app from configuration file
run Configure and start a permissive default sandbox
show Show live or local app configuration
ps List active instances
version Display version information
@ -69,7 +68,7 @@ Flags:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
out := new(bytes.Buffer)
c := buildCommand(t.Context(), container.NewMsg(nil), new(earlyHardeningErrs), out)
c := buildCommand(t.Context(), out)
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
t.Errorf("Parse: error = %v; want %v",
err, command.ErrHelp)

View File

@ -13,6 +13,8 @@ import (
"syscall"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
var (
@ -22,20 +24,20 @@ var (
license string
)
// earlyHardeningErrs are errors collected while setting up early hardening feature.
type earlyHardeningErrs struct{ yamaLSM, dumpable error }
func init() { hlog.Prepare("hakurei") }
func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(nil)
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
log.SetPrefix("hakurei: ")
log.SetFlags(0)
msg := container.NewMsg(log.Default())
if err := container.SetPtracer(0); err != nil {
hlog.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
// not fatal: this program runs as the privileged user
}
early := earlyHardeningErrs{
yamaLSM: container.SetPtracer(0),
dumpable: container.SetDumpable(container.SUID_DUMP_DISABLE),
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
// not fatal: this program runs as the privileged user
}
if os.Geteuid() == 0 {
@ -46,10 +48,10 @@ func main() {
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
buildCommand(ctx, msg, &early, os.Stderr).MustParse(os.Args[1:], func(err error) {
msg.Verbosef("command returned %v", err)
buildCommand(ctx, os.Stderr).MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
msg.BeforeExit()
hlog.BeforeExit()
os.Exit(0)
}
// this catches faulty command handlers that fail to return before this point

View File

@ -10,20 +10,20 @@ import (
"strings"
"syscall"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
)
func tryPath(msg container.Msg, name string) (config *hst.Config) {
func tryPath(name string) (config *hst.Config) {
var r io.Reader
config = new(hst.Config)
if name != "-" {
r = tryFd(msg, name)
r = tryFd(name)
if r == nil {
msg.Verbose("load configuration from file")
hlog.Verbose("load configuration from file")
if f, err := os.Open(name); err != nil {
log.Fatalf("cannot access configuration file %q: %s", name, err)
@ -49,14 +49,14 @@ func tryPath(msg container.Msg, name string) (config *hst.Config) {
return
}
func tryFd(msg container.Msg, name string) io.ReadCloser {
func tryFd(name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil {
if !errors.Is(err, strconv.ErrSyntax) {
msg.Verbosef("name cannot be interpreted as int64: %v", err)
hlog.Verbosef("name cannot be interpreted as int64: %v", err)
}
return nil
} else {
msg.Verbosef("trying config stream from %d", v)
hlog.Verbosef("trying config stream from %d", v)
fd := uintptr(v)
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
if errors.Is(errno, syscall.EBADF) {
@ -68,7 +68,7 @@ func tryFd(msg container.Msg, name string) io.ReadCloser {
}
}
func tryShort(msg container.Msg, name string) (config *hst.Config, entry *state.State) {
func tryShort(name string) (config *hst.Config, entry *state.State) {
likePrefix := false
if len(name) <= 32 {
likePrefix = true
@ -86,11 +86,11 @@ func tryShort(msg container.Msg, name string) (config *hst.Config, entry *state.
// try to match from state store
if likePrefix && len(name) >= 8 {
msg.Verbose("argument looks like prefix")
hlog.Verbose("argument looks like prefix")
var sc hst.Paths
app.CopyPaths().Copy(&sc, new(app.Hsu).MustID())
s := state.NewMulti(msg, sc.RunDirPath.String())
app.CopyPaths(&sc, new(app.Hsu).MustID())
s := state.NewMulti(sc.RunDirPath.String())
if entries, err := state.Join(s); err != nil {
log.Printf("cannot join store: %v", err)
// drop to fetch from file
@ -104,7 +104,7 @@ func tryShort(msg container.Msg, name string) (config *hst.Config, entry *state.
break
}
msg.Verbosef("instance %s skipped", v)
hlog.Verbosef("instance %s skipped", v)
}
}
}

View File

@ -11,10 +11,10 @@ import (
"text/tabwriter"
"time"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/system/dbus"
)
func printShowSystem(output io.Writer, short, flagJSON bool) {
@ -22,7 +22,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
defer t.MustFlush()
info := &hst.Info{User: new(app.Hsu).MustID()}
app.CopyPaths().Copy(&info.Paths, info.User)
app.CopyPaths(&info.Paths, info.User)
if flagJSON {
printJSON(output, short, info)
@ -39,9 +39,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
func printShowInstance(
output io.Writer, now time.Time,
instance *state.State, config *hst.Config,
short, flagJSON bool) (valid bool) {
valid = true
short, flagJSON bool) {
if flagJSON {
if instance != nil {
printJSON(output, short, instance)
@ -54,11 +52,8 @@ func printShowInstance(
t := newPrinter(output)
defer t.MustFlush()
if err := config.Validate(); err != nil {
valid = false
if m, ok := container.GetErrorMessage(err); ok {
mustPrint(output, "Error: "+m+"!\n\n")
}
if config.Container == nil {
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
}
if instance != nil {
@ -78,11 +73,11 @@ func printShowInstance(
if len(config.Groups) > 0 {
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
}
if config.Home != nil {
t.Printf(" Home:\t%s\n", config.Home)
}
if config.Container != nil {
params := config.Container
if params.Home != nil {
t.Printf(" Home:\t%s\n", params.Home)
}
if params.Hostname != "" {
t.Printf(" Hostname:\t%s\n", params.Hostname)
}
@ -105,12 +100,12 @@ func printShowInstance(
}
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
if params.Path != nil {
t.Printf(" Path:\t%s\n", params.Path)
if config.Path != nil {
t.Printf(" Path:\t%s\n", config.Path)
}
if len(params.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(params.Args, " "))
}
if len(config.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
}
t.Printf("\n")
@ -119,7 +114,6 @@ func printShowInstance(
t.Printf("Filesystem\n")
for _, f := range config.Container.Filesystem {
if !f.Valid() {
valid = false
t.Println(" <invalid>")
continue
}
@ -139,7 +133,7 @@ func printShowInstance(
}
}
printDBus := func(c *hst.BusConfig) {
printDBus := func(c *dbus.Config) {
t.Printf(" Filter:\t%v\n", c.Filter)
if len(c.See) > 0 {
t.Printf(" See:\t%q\n", c.See)
@ -167,8 +161,6 @@ func printShowInstance(
printDBus(config.SystemBus)
t.Printf("\n")
}
return
}
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {

View File

@ -7,6 +7,7 @@ import (
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/system/dbus"
)
var (
@ -26,14 +27,13 @@ var (
testAppTime = time.Unix(0, 9).UTC()
)
func TestPrintShowInstance(t *testing.T) {
func Test_printShowInstance(t *testing.T) {
testCases := []struct {
name string
instance *state.State
config *hst.Config
short, json bool
want string
valid bool
}{
{"config", nil, hst.Template(), false, false, `App
Identity: 9 (org.chromium.Chromium)
@ -71,25 +71,21 @@ System bus
Filter: true
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`, true},
{"config pd", nil, new(hst.Config), false, false, `Error: configuration missing container state!
`},
{"config pd", nil, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
App
Identity: 0
Enablements: (no enablements)
`, false},
{"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `Error: container configuration missing path to home directory!
App
`},
{"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `App
Identity: 0
Enablements: (no enablements)
Flags: none
`, false},
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfigJSON, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `Error: container configuration missing path to home directory!
App
`},
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfigJSON, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App
Identity: 0
Enablements: (no enablements)
Flags: none
@ -99,8 +95,8 @@ Filesystem
Extra ACL
`, false},
{"config pd dbus see", nil, &hst.Config{SessionBus: &hst.BusConfig{See: []string{"org.example.test"}}}, false, false, `Error: configuration missing container state!
`},
{"config pd dbus see", nil, &hst.Config{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}, false, false, `Warning: this configuration uses permissive defaults!
App
Identity: 0
@ -110,7 +106,7 @@ Session bus
Filter: false
See: ["org.example.test"]
`, false},
`},
{"instance", testState, hst.Template(), false, false, `State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
@ -152,8 +148,8 @@ System bus
Filter: true
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`, true},
{"instance pd", testState, new(hst.Config), false, false, `Error: configuration missing container state!
`},
{"instance pd", testState, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
@ -163,10 +159,10 @@ App
Identity: 0
Enablements: (no enablements)
`, false},
`},
{"json nil", nil, nil, false, true, `null
`, true},
`},
{"json instance", testState, nil, false, true, `{
"instance": [
142,
@ -189,6 +185,14 @@ App
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": {
"wayland": true,
"dbus": true,
@ -230,6 +234,9 @@ App
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
@ -252,6 +259,8 @@ App
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
@ -324,25 +333,22 @@ App
"dev": true,
"optional": true
}
],
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
]
}
},
"time": "1970-01-01T00:00:00.000000009Z"
}
`, true},
`},
{"json config", nil, hst.Template(), false, true, `{
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": {
"wayland": true,
"dbus": true,
@ -384,6 +390,9 @@ App
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
@ -406,6 +415,8 @@ App
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
@ -478,39 +489,26 @@ App
"dev": true,
"optional": true
}
],
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
]
}
}
`, true},
`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
output := new(strings.Builder)
gotValid := printShowInstance(output, testTime, tc.instance, tc.config, tc.short, tc.json)
printShowInstance(output, testTime, tc.instance, tc.config, tc.short, tc.json)
if got := output.String(); got != tc.want {
t.Errorf("printShowInstance: \n%s\nwant\n%s", got, tc.want)
t.Errorf("printShowInstance: got\n%s\nwant\n%s",
got, tc.want)
return
}
if gotValid != tc.valid {
t.Errorf("printShowInstance: valid = %v, want %v", gotValid, tc.valid)
}
})
}
}
func TestPrintPs(t *testing.T) {
func Test_printPs(t *testing.T) {
testCases := []struct {
name string
entries state.Entries
@ -553,6 +551,14 @@ func TestPrintPs(t *testing.T) {
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": {
"wayland": true,
"dbus": true,
@ -594,6 +600,9 @@ func TestPrintPs(t *testing.T) {
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
@ -616,6 +625,8 @@ func TestPrintPs(t *testing.T) {
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
@ -688,17 +699,6 @@ func TestPrintPs(t *testing.T) {
"dev": true,
"optional": true
}
],
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
]
}
},

View File

@ -5,9 +5,10 @@ import (
"log"
"os"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/system/dbus"
)
type appInfo struct {
@ -37,9 +38,9 @@ type appInfo struct {
// passed through to [hst.Config]
DirectWayland bool `json:"direct_wayland,omitempty"`
// passed through to [hst.Config]
SystemBus *hst.BusConfig `json:"system_bus,omitempty"`
SystemBus *dbus.Config `json:"system_bus,omitempty"`
// passed through to [hst.Config]
SessionBus *hst.BusConfig `json:"session_bus,omitempty"`
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// passed through to [hst.Config]
Enablements *hst.Enablements `json:"enablements,omitempty"`
@ -55,23 +56,30 @@ type appInfo struct {
// store path to nixGL source
NixGL string `json:"nix_gl,omitempty"`
// store path to activate-and-exec script
Launcher *check.Absolute `json:"launcher"`
Launcher *container.Absolute `json:"launcher"`
// store path to /run/current-system
CurrentSystem *check.Absolute `json:"current_system"`
CurrentSystem *container.Absolute `json:"current_system"`
// store path to home-manager activation package
ActivationPackage string `json:"activation_package"`
}
func (app *appInfo) toHst(pathSet *appPathSet, pathname *check.Absolute, argv []string, flagDropShell bool) *hst.Config {
func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, argv []string, flagDropShell bool) *hst.Config {
config := &hst.Config{
ID: app.ID,
Path: pathname,
Args: argv,
Enablements: app.Enablements,
SystemBus: app.SystemBus,
SessionBus: app.SessionBus,
DirectWayland: app.DirectWayland,
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Identity: app.Identity,
Groups: app.Groups,
@ -84,35 +92,33 @@ func (app *appInfo) toHst(pathSet *appPathSet, pathname *check.Absolute, argv []
Device: app.Device,
Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID,
Multiarch: app.Multiarch,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsTmp.Append("app")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Path: pathname,
Args: argv,
},
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
}
if app.Multiarch {
config.Container.SeccompFlags |= seccomp.AllowMultiarch
}
if app.Bluetooth {
config.Container.SeccompFlags |= seccomp.AllowBluetooth
}
return config
}

View File

@ -12,24 +12,23 @@ import (
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
var (
errSuccess = errors.New("success")
)
func main() {
log.SetPrefix("hpkg: ")
log.SetFlags(0)
msg := container.NewMsg(log.Default())
func init() {
hlog.Prepare("hpkg")
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)
}
}
func main() {
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
@ -42,7 +41,7 @@ func main() {
flagVerbose bool
flagDropShell bool
)
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { msg.SwapVerbose(flagVerbose); return nil }).
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
@ -81,22 +80,22 @@ func main() {
Extract package and set up for cleanup.
*/
var workDir *check.Absolute
var workDir *container.Absolute
if p, err := os.MkdirTemp("", "hpkg.*"); err != nil {
log.Printf("cannot create temporary directory: %v", err)
return err
} else if workDir, err = check.NewAbs(p); err != nil {
} else if workDir, err = container.NewAbs(p); err != nil {
log.Printf("invalid temporary directory: %v", err)
return err
}
cleanup := func() {
// should be faster than a native implementation
mustRun(msg, chmod, "-R", "+w", workDir.String())
mustRun(msg, rm, "-rf", workDir.String())
mustRun(chmod, "-R", "+w", workDir.String())
mustRun(rm, "-rf", workDir.String())
}
beforeRunFail.Store(&cleanup)
mustRun(msg, tar, "-C", workDir.String(), "-xf", pkgPath)
mustRun(tar, "-C", workDir.String(), "-xf", pkgPath)
/*
Parse bundle and app metadata, do pre-install checks.
@ -149,10 +148,10 @@ func main() {
}
// sec: should compare version string
msg.Verbosef("installing application %q version %q over local %q",
hlog.Verbosef("installing application %q version %q over local %q",
bundle.ID, bundle.Version, a.Version)
} else {
msg.Verbosef("application %q clean installation", bundle.ID)
hlog.Verbosef("application %q clean installation", bundle.ID)
// sec: should install credentials
}
@ -160,7 +159,7 @@ func main() {
Setup steps for files owned by the target user.
*/
withCacheDir(ctx, msg, "install", []string{
withCacheDir(ctx, "install", []string{
// export inner bundle path in the environment
"export BUNDLE=" + hst.Tmp + "/bundle",
// replace inner /etc
@ -182,7 +181,7 @@ func main() {
}, workDir, bundle, pathSet, flagDropShell, cleanup)
if bundle.GPU {
withCacheDir(ctx, msg, "mesa-wrappers", []string{
withCacheDir(ctx, "mesa-wrappers", []string{
// link nixGL mesa wrappers
"mkdir -p nix/.nixGL",
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
@ -194,7 +193,7 @@ func main() {
Activate home-manager generation.
*/
withNixDaemon(ctx, msg, "activate", []string{
withNixDaemon(ctx, "activate", []string{
// clean up broken links
"mkdir -p .local/state/{nix,home-manager}",
"chmod -R +w .local/state/{nix,home-manager}",
@ -262,7 +261,7 @@ func main() {
*/
if a.GPU && flagAutoDrivers {
withNixDaemon(ctx, msg, "nix-gl", []string{
withNixDaemon(ctx, "nix-gl", []string{
"mkdir -p /nix/.nixGL/auto",
"rm -rf /nix/.nixGL/auto",
"export NIXPKGS_ALLOW_UNFREE=1",
@ -276,12 +275,12 @@ func main() {
"path:" + a.NixGL + "#nixVulkanNvidia",
}, true, func(config *hst.Config) *hst.Config {
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
}...)
appendGPUFilesystem(config)
return config
@ -317,7 +316,7 @@ func main() {
Spawn app.
*/
mustRunApp(ctx, msg, config, func() {})
mustRunApp(ctx, config, func() {})
return errSuccess
}).
Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
@ -325,9 +324,9 @@ func main() {
}
c.MustParse(os.Args[1:], func(err error) {
msg.Verbosef("command returned %v", err)
hlog.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
msg.BeforeExit()
hlog.BeforeExit()
os.Exit(0)
}
})

View File

@ -8,36 +8,35 @@ import (
"sync/atomic"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
const bash = "bash"
var (
dataHome *check.Absolute
dataHome *container.Absolute
)
func init() {
// dataHome
if a, err := check.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil {
if a, err := container.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil {
dataHome = a
} else {
dataHome = fhs.AbsVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid()))
dataHome = container.AbsFHSVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid()))
}
}
var (
pathBin = fhs.AbsRoot.Append("bin")
pathBin = container.AbsFHSRoot.Append("bin")
pathNix = check.MustAbs("/nix/")
pathNix = container.MustAbs("/nix/")
pathNixStore = pathNix.Append("store/")
pathCurrentSystem = fhs.AbsRun.Append("current-system")
pathCurrentSystem = container.AbsFHSRun.Append("current-system")
pathSwBin = pathCurrentSystem.Append("sw/bin/")
pathShell = pathSwBin.Append(bash)
pathData = check.MustAbs("/data")
pathData = container.MustAbs("/data")
pathDataData = pathData.Append("data")
)
@ -52,8 +51,8 @@ func lookPath(file string) string {
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(msg container.Msg, name string, arg ...string) {
msg.Verbosef("spawning process: %q %q", name, arg)
func mustRun(name string, arg ...string) {
hlog.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
@ -66,15 +65,15 @@ func mustRun(msg container.Msg, name string, arg ...string) {
type appPathSet struct {
// ${dataHome}/${id}
baseDir *check.Absolute
baseDir *container.Absolute
// ${baseDir}/app
metaPath *check.Absolute
metaPath *container.Absolute
// ${baseDir}/files
homeDir *check.Absolute
homeDir *container.Absolute
// ${baseDir}/cache
cacheDir *check.Absolute
cacheDir *container.Absolute
// ${baseDir}/cache/nix
nixPath *check.Absolute
nixPath *container.Absolute
}
func pathSetByApp(id string) *appPathSet {
@ -90,28 +89,28 @@ func pathSetByApp(id string) *appPathSet {
func appendGPUFilesystem(config *hst.Config) {
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("dri"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}},
// mali
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("mali"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("mali0"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("umplock"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali0"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("umplock"), Device: true, Optional: true}},
// nvidia
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidiactl"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-modeset"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidiactl"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-modeset"), Device: true, Optional: true}},
// nvidia OpenCL/CUDA
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-uvm"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-uvm-tools"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm-tools"), Device: true, Optional: true}},
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia1"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia2"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia3"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia4"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia5"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia6"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia7"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia8"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia9"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia10"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia11"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia12"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia13"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia14"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia15"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia16"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia17"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia18"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia19"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia1"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia2"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia3"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia4"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia5"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia6"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia7"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia8"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia9"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia10"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia11"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia12"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia13"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia14"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia15"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia16"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia17"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia18"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia19"), Device: true, Optional: true}},
}...)
}

View File

@ -9,14 +9,14 @@ import (
"os"
"os/exec"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
var hakureiPath = internal.MustHakureiPath()
func mustRunApp(ctx context.Context, msg container.Msg, config *hst.Config, beforeFail func()) {
func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
var (
cmd *exec.Cmd
st io.WriteCloser
@ -26,10 +26,10 @@ func mustRunApp(ctx context.Context, msg container.Msg, config *hst.Config, befo
beforeFail()
log.Fatalf("cannot pipe: %v", err)
} else {
if msg.IsVerbose() {
cmd = exec.CommandContext(ctx, hakureiPath.String(), "-v", "app", "3")
if hlog.Load() {
cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3")
} else {
cmd = exec.CommandContext(ctx, hakureiPath.String(), "app", "3")
cmd = exec.CommandContext(ctx, hakureiPath, "app", "3")
}
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.ExtraFiles = []*os.File{r}
@ -51,8 +51,7 @@ func mustRunApp(ctx context.Context, msg container.Msg, config *hst.Config, befo
var exitError *exec.ExitError
if errors.As(err, &exitError) {
beforeFail()
msg.BeforeExit()
os.Exit(exitError.ExitCode())
internal.Exit(exitError.ExitCode())
} else {
beforeFail()
log.Fatalf("cannot wait: %v", err)

View File

@ -62,11 +62,11 @@ def check_state(name, enablements):
config = instance['config']
if len(config['container']['args']) != 1 or not (config['container']['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['container']['args'][0]):
raise Exception(f"unexpected args {config['container']['args']}")
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['args'][0]):
raise Exception(f"unexpected args {instance['config']['args']}")
if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {config['enablements']}")
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
start_all()

View File

@ -2,50 +2,22 @@ package main
import (
"context"
"os"
"strings"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/internal"
)
func withNixDaemon(
ctx context.Context,
msg container.Msg,
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
mustRunAppDropShell(ctx, msg, updateConfig(&hst.Config{
mustRunAppDropShell(ctx, updateConfig(&hst.Config{
ID: app.ID,
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
HostNet: net,
Multiarch: true,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Path: pathShell,
Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon
@ -58,18 +30,48 @@ func withNixDaemon(
// terminate nix-daemon
" && pkill nix-daemon",
},
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
HostNet: net,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
},
}), dropShell, beforeFail)
}
func withCacheDir(
ctx context.Context,
msg container.Msg,
action string, command []string, workDir *check.Absolute,
action string, command []string, workDir *container.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, msg, &hst.Config{
mustRunAppDropShell(ctx, &hst.Config{
ID: app.ID,
Path: pathShell,
Args: []string{bash, "-lc", strings.Join(command, " && ")},
Username: "nixos",
Shell: pathShell,
Home: pathDataData.Append(app.ID, "cache"),
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
@ -80,37 +82,27 @@ func withCacheDir(
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Multiarch: true,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: workDir.Append(fhs.Etc), Special: true}},
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: workDir.Append(container.FHSEtc), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID, "cache"), Source: pathSet.cacheDir, Write: true, Ensure: true}},
},
Username: "nixos",
Shell: pathShell,
Home: pathDataData.Append(app.ID, "cache"),
Path: pathShell,
Args: []string{bash, "-lc", strings.Join(command, " && ")},
},
}, dropShell, beforeFail)
}
func mustRunAppDropShell(ctx context.Context, msg container.Msg, config *hst.Config, dropShell bool, beforeFail func()) {
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
if dropShell {
if config.Container != nil {
config.Container.Args = []string{bash, "-l"}
}
mustRunApp(ctx, msg, config, beforeFail)
config.Args = []string{bash, "-l"}
mustRunApp(ctx, config, beforeFail)
beforeFail()
msg.BeforeExit()
os.Exit(0)
internal.Exit(0)
}
mustRunApp(ctx, msg, config, beforeFail)
mustRunApp(ctx, config, beforeFail)
}

View File

@ -1,7 +1,5 @@
package main
// minimise imports to avoid inadvertently calling init or global variable functions
import (
"bytes"
"fmt"
@ -21,9 +19,6 @@ const (
envGroups = "HAKUREI_GROUPS"
PR_SET_NO_NEW_PRIVS = 0x26
identityMin = 0
identityMax = 9999
)
func main() {
@ -34,9 +29,6 @@ func main() {
if os.Geteuid() != 0 {
log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
}
if os.Getegid() != os.Getgid() {
log.Fatal("this program must not have the setgid bit set")
}
puid := os.Getuid()
if puid == 0 {
@ -99,7 +91,7 @@ func main() {
// 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 < identityMin || identity > identityMax {
} else if identity, err := parseUint32Fast(as); err != nil || identity < 0 || identity > 9999 {
log.Fatal("invalid identity")
} else {
uid += identity

View File

@ -1,5 +1,4 @@
// Package check provides types yielding values checked to meet a condition.
package check
package container
import (
"encoding/json"
@ -12,7 +11,9 @@ import (
)
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
type AbsoluteError struct{ Pathname string }
type AbsoluteError struct {
Pathname string
}
func (e *AbsoluteError) Error() string { return fmt.Sprintf("path %q is not absolute", e.Pathname) }
func (e *AbsoluteError) Is(target error) bool {
@ -24,13 +25,15 @@ func (e *AbsoluteError) Is(target error) bool {
}
// Absolute holds a pathname checked to be absolute.
type Absolute struct{ pathname string }
type Absolute struct {
pathname string
}
// unsafeAbs returns [check.Absolute] on any string value.
func unsafeAbs(pathname string) *Absolute { return &Absolute{pathname} }
// isAbs wraps [path.IsAbs] in case additional checks are added in the future.
func isAbs(pathname string) bool { return path.IsAbs(pathname) }
func (a *Absolute) String() string {
if a.pathname == "" {
if a.pathname == zeroString {
panic("attempted use of zero Absolute")
}
return a.pathname
@ -41,16 +44,16 @@ func (a *Absolute) Is(v *Absolute) bool {
return true
}
return a != nil && v != nil &&
a.pathname != "" && v.pathname != "" &&
a.pathname != zeroString && v.pathname != zeroString &&
a.pathname == v.pathname
}
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
func NewAbs(pathname string) (*Absolute, error) {
if !path.IsAbs(pathname) {
if !isAbs(pathname) {
return nil, &AbsoluteError{pathname}
}
return unsafeAbs(pathname), nil
return &Absolute{pathname}, nil
}
// MustAbs calls [NewAbs] and panics on error.
@ -64,16 +67,16 @@ func MustAbs(pathname string) *Absolute {
// Append calls [path.Join] with [Absolute] as the first element.
func (a *Absolute) Append(elem ...string) *Absolute {
return unsafeAbs(path.Join(append([]string{a.String()}, elem...)...))
return &Absolute{path.Join(append([]string{a.String()}, elem...)...)}
}
// Dir calls [path.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return unsafeAbs(path.Dir(a.String())) }
func (a *Absolute) Dir() *Absolute { return &Absolute{path.Dir(a.String())} }
func (a *Absolute) GobEncode() ([]byte, error) { return []byte(a.String()), nil }
func (a *Absolute) GobDecode(data []byte) error {
pathname := string(data)
if !path.IsAbs(pathname) {
if !isAbs(pathname) {
return &AbsoluteError{pathname}
}
a.pathname = pathname
@ -86,7 +89,7 @@ func (a *Absolute) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &pathname); err != nil {
return err
}
if !path.IsAbs(pathname) {
if !isAbs(pathname) {
return &AbsoluteError{pathname}
}
a.pathname = pathname

View File

@ -1,4 +1,4 @@
package check_test
package container
import (
"bytes"
@ -9,14 +9,8 @@ import (
"strings"
"syscall"
"testing"
_ "unsafe"
. "hakurei.app/container/check"
)
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
func unsafeAbs(_ string) *Absolute
func TestAbsoluteError(t *testing.T) {
testCases := []struct {
name string
@ -86,7 +80,7 @@ func TestNewAbs(t *testing.T) {
func TestAbsoluteString(t *testing.T) {
t.Run("passthrough", func(t *testing.T) {
pathname := "/etc"
if got := unsafeAbs(pathname).String(); got != pathname {
if got := (&Absolute{pathname}).String(); got != pathname {
t.Errorf("String: %q, want %q", got, pathname)
}
})

View File

@ -3,18 +3,15 @@ package container
import (
"encoding/gob"
"fmt"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
func init() { gob.Register(new(AutoEtcOp)) }
// Etc appends an [Op] that expands host /etc into a toplevel symlink mirror with /etc semantics.
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Etc(host *check.Absolute, prefix string) *Ops {
func (f *Ops) Etc(host *Absolute, prefix string) *Ops {
e := &AutoEtcOp{prefix}
f.Mkdir(fhs.AbsEtc, 0755)
f.Mkdir(AbsFHSEtc, 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)
return f
@ -30,7 +27,7 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
}
state.nonrepeatable |= nrAutoEtc
const target = sysrootPath + fhs.Etc
const target = sysrootPath + FHSEtc
rel := e.hostRel() + "/"
if err := k.mkdirAll(target, 0755); err != nil {
@ -45,7 +42,7 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
case ".host", "passwd", "group":
case "mtab":
if err = k.symlink(fhs.Proc+"mounts", target+n); err != nil {
if err = k.symlink(FHSProc+"mounts", target+n); err != nil {
return err
}
@ -60,7 +57,7 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
return nil
}
func (e *AutoEtcOp) hostPath() *check.Absolute { return fhs.AbsEtc.Append(e.hostRel()) }
func (e *AutoEtcOp) hostPath() *Absolute { return AbsFHSEtc.Append(e.hostRel()) }
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtcOp) Is(op Op) bool {

View File

@ -5,7 +5,6 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
@ -257,11 +256,11 @@ func TestAutoEtcOp(t *testing.T) {
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"pd", new(Ops).Etc(check.MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
&MkdirOp{Path: check.MustAbs("/etc/"), Perm: 0755},
{"pd", new(Ops).Etc(MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
&MkdirOp{Path: MustAbs("/etc/"), Perm: 0755},
&BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
},
&AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"},
}},

View File

@ -3,22 +3,19 @@ package container
import (
"encoding/gob"
"fmt"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
func init() { gob.Register(new(AutoRootOp)) }
// Root appends an [Op] that expands a directory into a toplevel bind mount mirror on container root.
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Root(host *check.Absolute, flags int) *Ops {
func (f *Ops) Root(host *Absolute, flags int) *Ops {
*f = append(*f, &AutoRootOp{host, flags, nil})
return f
}
type AutoRootOp struct {
Host *check.Absolute
Host *Absolute
// passed through to bindMount
Flags int
@ -37,11 +34,11 @@ func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
r.resolved = make([]*BindMountOp, 0, len(d))
for _, ent := range d {
name := ent.Name()
if IsAutoRootBindable(state, name) {
if IsAutoRootBindable(name) {
// careful: the Valid method is skipped, make sure this is always valid
op := &BindMountOp{
Source: r.Host.Append(name),
Target: fhs.AbsRoot.Append(name),
Target: AbsFHSRoot.Append(name),
Flags: r.Flags,
}
if err = op.early(state, k); err != nil {
@ -81,7 +78,7 @@ func (r *AutoRootOp) String() string {
}
// IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot.
func IsAutoRootBindable(msg Msg, name string) bool {
func IsAutoRootBindable(name string) bool {
switch name {
case "proc", "dev", "tmp", "mnt", "etc":

View File

@ -5,8 +5,6 @@ import (
"os"
"testing"
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
@ -20,15 +18,15 @@ func TestAutoRootOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable,
Host: MustAbs("/"),
Flags: BindWritable,
}, []stub.Call{
call("readdir", stub.ExpectArgs{"/"}, stubDir(), stub.UniqueError(2)),
}, stub.UniqueError(2), nil, nil},
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable,
Host: MustAbs("/"),
Flags: BindWritable,
}, []stub.Call{
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
@ -36,8 +34,8 @@ func TestAutoRootOp(t *testing.T) {
}, stub.UniqueError(1), nil, nil},
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable,
Host: MustAbs("/"),
Flags: BindWritable,
}, []stub.Call{
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
@ -57,8 +55,8 @@ func TestAutoRootOp(t *testing.T) {
}, stub.UniqueError(0)},
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable,
Host: MustAbs("/"),
Flags: BindWritable,
}, []stub.Call{
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
@ -88,7 +86,7 @@ func TestAutoRootOp(t *testing.T) {
}, nil},
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"),
}, []stub.Call{
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),
@ -121,14 +119,14 @@ func TestAutoRootOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*AutoRootOp)(nil), false},
{"zero", new(AutoRootOp), false},
{"valid", &AutoRootOp{Host: check.MustAbs("/")}, true},
{"valid", &AutoRootOp{Host: MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"pd", new(Ops).Root(check.MustAbs("/"), bits.BindWritable), Ops{
{"pd", new(Ops).Root(MustAbs("/"), BindWritable), Ops{
&AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable,
Host: MustAbs("/"),
Flags: BindWritable,
},
}},
})
@ -137,43 +135,43 @@ func TestAutoRootOp(t *testing.T) {
{"zero", new(AutoRootOp), new(AutoRootOp), false},
{"internal ne", &AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable,
Host: MustAbs("/"),
Flags: BindWritable,
}, &AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable,
Host: MustAbs("/"),
Flags: BindWritable,
resolved: []*BindMountOp{new(BindMountOp)},
}, true},
{"flags differs", &AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable | bits.BindDevice,
Host: MustAbs("/"),
Flags: BindWritable | BindDevice,
}, &AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable,
Host: MustAbs("/"),
Flags: BindWritable,
}, false},
{"host differs", &AutoRootOp{
Host: check.MustAbs("/tmp/"),
Flags: bits.BindWritable,
Host: MustAbs("/tmp/"),
Flags: BindWritable,
}, &AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable,
Host: MustAbs("/"),
Flags: BindWritable,
}, false},
{"equals", &AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable,
Host: MustAbs("/"),
Flags: BindWritable,
}, &AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable,
Host: MustAbs("/"),
Flags: BindWritable,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"root", &AutoRootOp{
Host: check.MustAbs("/"),
Flags: bits.BindWritable,
Host: MustAbs("/"),
Flags: BindWritable,
}, "setting up", `auto root "/" flags 0x2`},
})
}
@ -182,26 +180,19 @@ func TestIsAutoRootBindable(t *testing.T) {
testCases := []struct {
name string
want bool
log bool
}{
{"proc", false, false},
{"dev", false, false},
{"tmp", false, false},
{"mnt", false, false},
{"etc", false, false},
{"", false, true},
{"proc", false},
{"dev", false},
{"tmp", false},
{"mnt", false},
{"etc", false},
{"", false},
{"var", true, false},
{"var", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var msg Msg
if tc.log {
msg = &kstub{nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),
}})}
}
if got := IsAutoRootBindable(msg, tc.name); got != tc.want {
if got := IsAutoRootBindable(tc.name); got != tc.want {
t.Errorf("IsAutoRootBindable: %v, want %v", got, tc.want)
}
})

View File

@ -1,13 +0,0 @@
// Package bits contains constants for configuring the container.
package bits
const (
// BindOptional skips nonexistent host paths.
BindOptional = 1 << iota
// BindWritable mounts filesystem read-write.
BindWritable
// BindDevice allows access to devices (special files) on this filesystem.
BindDevice
// BindEnsure attempts to create the host path if it does not exist.
BindEnsure
)

View File

@ -1,20 +0,0 @@
package bits
// FilterPreset specifies parts of the syscall filter preset to enable.
type FilterPreset int
const (
// PresetExt are project-specific extensions.
PresetExt FilterPreset = 1 << iota
// PresetDenyNS denies namespace setup syscalls.
PresetDenyNS
// PresetDenyTTY denies faking input.
PresetDenyTTY
// PresetDenyDevel denies development-related syscalls.
PresetDenyDevel
// PresetLinux32 sets PER_LINUX32.
PresetLinux32
// PresetStrict is a strict preset useful as a default value.
PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel
)

View File

@ -1,29 +0,0 @@
package check
import "strings"
const (
// SpecialOverlayEscape is the escape string for overlay mount options.
SpecialOverlayEscape = `\`
// SpecialOverlayOption is the separator string between overlay mount options.
SpecialOverlayOption = ","
// SpecialOverlayPath is the separator string between overlay paths.
SpecialOverlayPath = ":"
)
// EscapeOverlayDataSegment escapes a string for formatting into the data argument of an overlay mount call.
func EscapeOverlayDataSegment(s string) string {
if s == "" {
return ""
}
if f := strings.SplitN(s, "\x00", 2); len(f) > 0 {
s = f[0]
}
return strings.NewReplacer(
SpecialOverlayEscape, SpecialOverlayEscape+SpecialOverlayEscape,
SpecialOverlayOption, SpecialOverlayEscape+SpecialOverlayOption,
SpecialOverlayPath, SpecialOverlayEscape+SpecialOverlayPath,
).Replace(s)
}

View File

@ -1,27 +0,0 @@
package check_test
import (
"testing"
"hakurei.app/container/check"
)
func TestEscapeOverlayDataSegment(t *testing.T) {
testCases := []struct {
name string
s string
want string
}{
{"zero", "", ""},
{"multi", `\\\:,:,\\\`, `\\\\\\\:\,\:\,\\\\\\`},
{"bwrap", `/path :,\`, `/path \:\,\\`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := check.EscapeOverlayDataSegment(tc.s); got != tc.want {
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
}
})
}
}

View File

@ -14,9 +14,6 @@ import (
. "syscall"
"time"
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
)
@ -52,18 +49,17 @@ type (
cmd *exec.Cmd
ctx context.Context
msg Msg
Params
}
// Params holds container configuration and is safe to serialise.
Params struct {
// Working directory in the container.
Dir *check.Absolute
Dir *Absolute
// Initial process environment.
Env []string
// Pathname of initial process in the container.
Path *check.Absolute
Path *Absolute
// Initial process argv.
Args []string
// Deliver SIGINT to the initial process on context cancellation.
@ -85,7 +81,7 @@ type (
// Extra seccomp flags.
SeccompFlags seccomp.ExportFlag
// Seccomp presets. Has no effect unless SeccompRules is zero-length.
SeccompPresets bits.FilterPreset
SeccompPresets seccomp.FilterPreset
// Do not load seccomp program.
SeccompDisable bool
@ -166,14 +162,14 @@ func (p *Container) Start() error {
// map to overflow id to work around ownership checks
if p.Uid < 1 {
p.Uid = OverflowUid(p.msg)
p.Uid = OverflowUid()
}
if p.Gid < 1 {
p.Gid = OverflowGid(p.msg)
p.Gid = OverflowGid()
}
if !p.RetainSession {
p.SeccompPresets |= bits.PresetDenyTTY
p.SeccompPresets |= seccomp.PresetDenyTTY
}
if p.AdoptWaitDelay == 0 {
@ -201,7 +197,7 @@ func (p *Container) Start() error {
} else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
}
p.cmd.Dir = fhs.Root
p.cmd.Dir = FHSRoot
p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession,
Pdeathsig: SIGKILL,
@ -267,19 +263,19 @@ func (p *Container) Start() error {
}
return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
} else {
p.msg.Verbosef("landlock abi version %d", abi)
msg.Verbosef("landlock abi version %d", abi)
}
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
return &StartError{true, "create landlock ruleset", err, false, false}
} else {
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd)
return &StartError{true, "enforce landlock ruleset", err, false, false}
}
if err = Close(rulesetFd); err != nil {
p.msg.Verbosef("cannot close landlock ruleset: %v", err)
msg.Verbosef("cannot close landlock ruleset: %v", err)
// not fatal
}
}
@ -287,7 +283,7 @@ func (p *Container) Start() error {
landlockOut:
}
p.msg.Verbose("starting container init")
msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil {
return &StartError{false, "start container init", err, false, true}
}
@ -317,7 +313,7 @@ func (p *Container) Serve() error {
// do not transmit nil
if p.Dir == nil {
p.Dir = fhs.AbsRoot
p.Dir = AbsFHSRoot
}
if p.SeccompRules == nil {
p.SeccompRules = make([]seccomp.NativeRule, 0)
@ -329,7 +325,7 @@ func (p *Container) Serve() error {
Getuid(),
Getgid(),
len(p.ExtraFiles),
p.msg.IsVerbose(),
msg.IsVerbose(),
},
)
if err != nil {
@ -396,21 +392,17 @@ func (p *Container) ProcessState() *os.ProcessState {
}
// New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context, msg Msg) *Container {
if msg == nil {
msg = NewMsg(nil)
}
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}
func New(ctx context.Context) *Container {
p := &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
c, cancel := context.WithCancel(ctx)
p.cancel = cancel
p.cmd = exec.CommandContext(c, MustExecutable(msg))
p.cmd = exec.CommandContext(c, MustExecutable())
return p
}
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
func NewCommand(ctx context.Context, msg Msg, pathname *check.Absolute, name string, args ...string) *Container {
z := New(ctx, msg)
func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container {
z := New(ctx)
z.Path = pathname
z.Args = append([]string{name}, args...)
return z

View File

@ -20,11 +20,11 @@ import (
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/seccomp"
"hakurei.app/container/vfs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
)
@ -201,20 +201,20 @@ var containerTestCases = []struct {
rules []seccomp.NativeRule
flags seccomp.ExportFlag
presets bits.FilterPreset
presets seccomp.FilterPreset
}{
{"minimal", true, false, false, true,
emptyOps, emptyMnt,
1000, 100, nil, 0, bits.PresetStrict},
1000, 100, nil, 0, seccomp.PresetStrict},
{"allow", true, true, true, false,
emptyOps, emptyMnt,
1000, 100, nil, 0, bits.PresetExt | bits.PresetDenyDevel},
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
{"no filter", false, true, true, true,
emptyOps, emptyMnt,
1000, 100, nil, 0, bits.PresetExt},
1000, 100, nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true, false,
emptyOps, emptyMnt,
1, 31, []seccomp.NativeRule{{Syscall: seccomp.ScmpSyscall(syscall.SYS_SETUID), Errno: seccomp.ScmpErrno(syscall.EPERM)}}, 0, bits.PresetExt},
1, 31, []seccomp.NativeRule{{Syscall: seccomp.ScmpSyscall(syscall.SYS_SETUID), Errno: seccomp.ScmpErrno(syscall.EPERM)}}, 0, seccomp.PresetExt},
{"tmpfs", true, false, false, true,
earlyOps(new(container.Ops).
@ -223,11 +223,11 @@ var containerTestCases = []struct {
earlyMnt(
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
),
9, 9, nil, 0, bits.PresetStrict},
9, 9, nil, 0, seccomp.PresetStrict},
{"dev", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops).
Dev(check.MustAbs("/dev"), true),
Dev(container.MustAbs("/dev"), true),
),
earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
@ -241,11 +241,11 @@ var containerTestCases = []struct {
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
),
1971, 100, nil, 0, bits.PresetStrict},
1971, 100, nil, 0, seccomp.PresetStrict},
{"dev no mqueue", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops).
Dev(check.MustAbs("/dev"), false),
Dev(container.MustAbs("/dev"), false),
),
earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
@ -258,17 +258,17 @@ var containerTestCases = []struct {
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
),
1971, 100, nil, 0, bits.PresetStrict},
1971, 100, nil, 0, seccomp.PresetStrict},
{"overlay", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := check.MustAbs(t.TempDir())
tempDir := container.MustAbs(t.TempDir())
lower0, lower1, upper, work :=
tempDir.Append("lower0"),
tempDir.Append("lower1"),
tempDir.Append("upper"),
tempDir.Append("work")
for _, a := range []*check.Absolute{lower0, lower1, upper, work} {
for _, a := range []*container.Absolute{lower0, lower1, upper, work} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
@ -286,24 +286,24 @@ var containerTestCases = []struct {
return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay",
"rw,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",upperdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*check.Absolute).String())+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+
",workdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*check.Absolute).String())+
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+
",redirect_dir=nofollow,uuid=on,userxattr"),
}
},
1 << 3, 1 << 14, nil, 0, bits.PresetStrict},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
{"overlay ephemeral", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := check.MustAbs(t.TempDir())
tempDir := container.MustAbs(t.TempDir())
lower0, lower1 :=
tempDir.Append("lower0"),
tempDir.Append("lower1")
for _, a := range []*check.Absolute{lower0, lower1} {
for _, a := range []*container.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
@ -319,15 +319,15 @@ var containerTestCases = []struct {
ent("/", hst.Tmp, "rw", "overlay", "overlay", ignore),
}
},
1 << 3, 1 << 14, nil, 0, bits.PresetStrict},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
{"overlay readonly", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := check.MustAbs(t.TempDir())
tempDir := container.MustAbs(t.TempDir())
lower0, lower1 :=
tempDir.Append("lower0"),
tempDir.Append("lower1")
for _, a := range []*check.Absolute{lower0, lower1} {
for _, a := range []*container.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
@ -342,15 +342,17 @@ var containerTestCases = []struct {
return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay",
"ro,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",redirect_dir=nofollow,userxattr"),
}
},
1 << 3, 1 << 14, nil, 0, bits.PresetStrict},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
}
func TestContainer(t *testing.T) {
replaceOutput(t)
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled
wantExitCode := 0
@ -390,7 +392,7 @@ func TestContainer(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel()
var libPaths []*check.Absolute
var libPaths []*container.Absolute
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid
c.Gid = tc.gid
@ -411,11 +413,11 @@ func TestContainer(t *testing.T) {
c.HostNet = tc.net
c.
Readonly(check.MustAbs(pathReadonly), 0755).
Tmpfs(check.MustAbs("/tmp"), 0, 0755).
Place(check.MustAbs("/etc/hostname"), []byte(c.Hostname))
Readonly(container.MustAbs(pathReadonly), 0755).
Tmpfs(container.MustAbs("/tmp"), 0, 0755).
Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname))
// needs /proc to check mountinfo
c.Proc(check.MustAbs("/proc"))
c.Proc(container.MustAbs("/proc"))
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
@ -446,10 +448,10 @@ func TestContainer(t *testing.T) {
_, _ = output.WriteTo(os.Stdout)
t.Fatalf("cannot serialise expected mount points: %v", err)
}
c.Place(check.MustAbs(pathWantMnt), want.Bytes())
c.Place(container.MustAbs(pathWantMnt), want.Bytes())
if tc.ro {
c.Remount(check.MustAbs("/"), syscall.MS_RDONLY)
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
}
if err := c.Start(); err != nil {
@ -545,13 +547,12 @@ func testContainerCancel(
}
func TestContainerString(t *testing.T) {
msg := container.NewMsg(nil)
c := container.NewCommand(t.Context(), msg, check.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
c.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset(
bits.PresetExt|bits.PresetDenyNS|bits.PresetDenyTTY,
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
c.SeccompFlags)
c.SeccompPresets = bits.PresetStrict
c.SeccompPresets = seccomp.PresetStrict
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
if got := c.String(); got != want {
t.Errorf("String: %s, want %s", got, want)
@ -682,13 +683,13 @@ const (
)
var (
absHelperInnerPath = check.MustAbs(helperInnerPath)
absHelperInnerPath = container.MustAbs(helperInnerPath)
)
var helperCommands []func(c command.Command)
func TestMain(m *testing.M) {
container.TryArgv0(nil)
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
if os.Getenv(envDoCheck) == "1" {
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
@ -710,14 +711,13 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
msg := container.NewMsg(nil)
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) {
c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(check.MustAbs(os.Args[0]), absHelperInnerPath, 0)
c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0)
// in case test has cgo enabled
if entries, err := ldd.Exec(ctx, msg, os.Args[0]); err != nil {
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
*libPaths = ldd.Path(entries)
@ -730,5 +730,5 @@ func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute
}
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
return helperNewContainerLibPaths(ctx, new([]*check.Absolute), args...)
return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...)
}

View File

@ -3,6 +3,7 @@ package container
import (
"io"
"io/fs"
"log"
"os"
"os/exec"
"os/signal"
@ -37,7 +38,7 @@ type syscallDispatcher interface {
setNoNewPrivs() error
// lastcap provides [LastCap].
lastcap(msg Msg) uintptr
lastcap() uintptr
// capset provides capset.
capset(hdrp *capHeader, datap *[2]capData) error
// capBoundingSetDrop provides capBoundingSetDrop.
@ -52,9 +53,9 @@ type syscallDispatcher interface {
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// bindMount provides procPaths.bindMount.
bindMount(msg Msg, source, target string, flags uintptr) error
bindMount(source, target string, flags uintptr) error
// remount provides procPaths.remount.
remount(msg Msg, target string, flags uintptr) error
remount(target string, flags uintptr) error
// mountTmpfs provides mountTmpfs.
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
// ensureFile provides ensureFile.
@ -121,12 +122,22 @@ type syscallDispatcher interface {
// wait4 provides syscall.Wait4
wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)
// printf provides the Printf method of [log.Logger].
printf(msg Msg, format string, v ...any)
// fatal provides the Fatal method of [log.Logger]
fatal(msg Msg, v ...any)
// fatalf provides the Fatalf method of [log.Logger]
fatalf(msg Msg, format string, v ...any)
// printf provides [log.Printf].
printf(format string, v ...any)
// fatal provides [log.Fatal]
fatal(v ...any)
// fatalf provides [log.Fatalf]
fatalf(format string, v ...any)
// verbose provides [Msg.Verbose].
verbose(v ...any)
// verbosef provides [Msg.Verbosef].
verbosef(format string, v ...any)
// suspend provides [Msg.Suspend].
suspend()
// resume provides [Msg.Resume].
resume() bool
// beforeExit provides [Msg.BeforeExit].
beforeExit()
}
// direct implements syscallDispatcher on the current kernel.
@ -140,7 +151,7 @@ func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
func (direct) lastcap(msg Msg) uintptr { return LastCap(msg) }
func (direct) lastcap() uintptr { return LastCap() }
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
@ -150,11 +161,11 @@ func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return Receive(key, e, fdp)
}
func (direct) bindMount(msg Msg, source, target string, flags uintptr) error {
return hostProc.bindMount(msg, source, target, flags)
func (direct) bindMount(source, target string, flags uintptr) error {
return hostProc.bindMount(source, target, flags)
}
func (direct) remount(msg Msg, target string, flags uintptr) error {
return hostProc.remount(msg, target, flags)
func (direct) remount(target string, flags uintptr) error {
return hostProc.remount(target, flags)
}
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
return mountTmpfs(k, fsname, target, flags, size, perm)
@ -221,6 +232,11 @@ func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *s
return syscall.Wait4(pid, wstatus, options, rusage)
}
func (direct) printf(msg Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) }
func (direct) fatal(msg Msg, v ...any) { msg.GetLogger().Fatal(v...) }
func (direct) fatalf(msg Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) }
func (direct) printf(format string, v ...any) { log.Printf(format, v...) }
func (direct) fatal(v ...any) { log.Fatal(v...) }
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }
func (direct) verbose(v ...any) { msg.Verbose(v...) }
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
func (direct) suspend() { msg.Suspend() }
func (direct) resume() bool { return msg.Resume() }
func (direct) beforeExit() { msg.BeforeExit() }

View File

@ -2,10 +2,8 @@ package container
import (
"bytes"
"fmt"
"io"
"io/fs"
"log"
"os"
"os/exec"
"reflect"
@ -138,7 +136,7 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
type simpleTestCase struct {
name string
f func(k *kstub) error
f func(k syscallDispatcher) error
want stub.Expect
wantErr error
}
@ -187,11 +185,11 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
state := &setupState{Params: tc.params}
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)},
)}
state := &setupState{Params: tc.params, Msg: k}
defer stub.HandleExit(t)
errEarly := tc.op.early(state, k)
k.Expects(stub.CallSeparator)
@ -329,11 +327,7 @@ func (k *kstub) setDumpable(dumpable uintptr) error {
}
func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err }
func (k *kstub) lastcap(msg Msg) uintptr {
k.Helper()
k.checkMsg(msg)
return k.Expects("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 {
k.Helper()
@ -409,18 +403,16 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
return
}
func (k *kstub) bindMount(msg Msg, source, target string, flags uintptr) error {
func (k *kstub) bindMount(source, target string, flags uintptr) error {
k.Helper()
k.checkMsg(msg)
return k.Expects("bindMount").Error(
stub.CheckArg(k.Stub, "source", source, 0),
stub.CheckArg(k.Stub, "target", target, 1),
stub.CheckArg(k.Stub, "flags", flags, 2))
}
func (k *kstub) remount(msg Msg, target string, flags uintptr) error {
func (k *kstub) remount(target string, flags uintptr) error {
k.Helper()
k.checkMsg(msg)
return k.Expects("remount").Error(
stub.CheckArg(k.Stub, "target", target, 0),
stub.CheckArg(k.Stub, "flags", flags, 1))
@ -702,7 +694,7 @@ func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage
return
}
func (k *kstub) printf(_ Msg, format string, v ...any) {
func (k *kstub) printf(format string, v ...any) {
k.Helper()
if k.Expects("printf").Error(
stub.CheckArg(k.Stub, "format", format, 0),
@ -711,7 +703,7 @@ func (k *kstub) printf(_ Msg, format string, v ...any) {
}
}
func (k *kstub) fatal(_ Msg, v ...any) {
func (k *kstub) fatal(v ...any) {
k.Helper()
if k.Expects("fatal").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
@ -720,7 +712,7 @@ func (k *kstub) fatal(_ Msg, v ...any) {
panic(stub.PanicExit)
}
func (k *kstub) fatalf(_ Msg, format string, v ...any) {
func (k *kstub) fatalf(format string, v ...any) {
k.Helper()
if k.Expects("fatalf").Error(
stub.CheckArg(k.Stub, "format", format, 0),
@ -730,35 +722,7 @@ func (k *kstub) fatalf(_ Msg, format string, v ...any) {
panic(stub.PanicExit)
}
func (k *kstub) checkMsg(msg Msg) {
k.Helper()
var target *kstub
if state, ok := msg.(*setupState); ok {
target = state.Msg.(*kstub)
} else {
target = msg.(*kstub)
}
if k != target {
panic(fmt.Sprintf("unexpected Msg: %#v", msg))
}
}
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
func (k *kstub) IsVerbose() bool { panic("unreachable") }
func (k *kstub) SwapVerbose(verbose bool) bool {
k.Helper()
expect := k.Expects("swapVerbose")
if expect.Error(
stub.CheckArg(k.Stub, "verbose", verbose, 0)) != nil {
k.FailNow()
}
return expect.Ret.(bool)
}
func (k *kstub) Verbose(v ...any) {
func (k *kstub) verbose(v ...any) {
k.Helper()
if k.Expects("verbose").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
@ -766,7 +730,7 @@ func (k *kstub) Verbose(v ...any) {
}
}
func (k *kstub) Verbosef(format string, v ...any) {
func (k *kstub) verbosef(format string, v ...any) {
k.Helper()
if k.Expects("verbosef").Error(
stub.CheckArg(k.Stub, "format", format, 0),
@ -775,6 +739,6 @@ func (k *kstub) Verbosef(format string, v ...any) {
}
}
func (k *kstub) Suspend() bool { k.Helper(); return k.Expects("suspend").Ret.(bool) }
func (k *kstub) Resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
func (k *kstub) BeforeExit() { k.Helper(); k.Expects("beforeExit") }
func (k *kstub) suspend() { k.Helper(); k.Expects("suspend") }
func (k *kstub) resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
func (k *kstub) beforeExit() { k.Helper(); k.Expects("beforeExit") }

View File

@ -5,7 +5,6 @@ import (
"os"
"syscall"
"hakurei.app/container/check"
"hakurei.app/container/vfs"
)
@ -17,7 +16,7 @@ func messageFromError(err error) (string, bool) {
if m, ok := messagePrefixP[os.PathError]("cannot ", err); ok {
return m, ok
}
if m, ok := messagePrefixP[check.AbsoluteError]("", err); ok {
if m, ok := messagePrefixP[AbsoluteError]("", err); ok {
return m, ok
}
if m, ok := messagePrefix[OpRepeatError]("", err); ok {

View File

@ -8,7 +8,6 @@ import (
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/container/vfs"
)
@ -35,7 +34,7 @@ func TestMessageFromError(t *testing.T) {
Err: stub.UniqueError(0xdeadbeef),
}, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true},
{"absolute", &check.AbsoluteError{Pathname: "etc/mtab"},
{"absolute", &AbsoluteError{"etc/mtab"},
`path "etc/mtab" is not absolute`, true},
{"repeat", OpRepeatError("autoetc"),

View File

@ -1,6 +1,7 @@
package container
import (
"log"
"os"
"sync"
)
@ -10,16 +11,16 @@ var (
executableOnce sync.Once
)
func copyExecutable(msg Msg) {
func copyExecutable() {
if name, err := os.Executable(); err != nil {
msg.BeforeExit()
msg.GetLogger().Fatalf("cannot read executable path: %v", err)
log.Fatalf("cannot read executable path: %v", err)
} else {
executable = name
}
}
func MustExecutable(msg Msg) string {
executableOnce.Do(func() { copyExecutable(msg) })
func MustExecutable() string {
executableOnce.Do(copyExecutable)
return executable
}

View File

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

View File

@ -1,41 +0,0 @@
package fhs
import (
_ "unsafe"
"hakurei.app/container/check"
)
/* constants in this file bypass abs check, be extremely careful when changing them! */
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
func unsafeAbs(_ string) *check.Absolute
var (
// AbsRoot is [Root] as [check.Absolute].
AbsRoot = unsafeAbs(Root)
// AbsEtc is [Etc] as [check.Absolute].
AbsEtc = unsafeAbs(Etc)
// AbsTmp is [Tmp] as [check.Absolute].
AbsTmp = unsafeAbs(Tmp)
// AbsRun is [Run] as [check.Absolute].
AbsRun = unsafeAbs(Run)
// AbsRunUser is [RunUser] as [check.Absolute].
AbsRunUser = unsafeAbs(RunUser)
// AbsUsrBin is [UsrBin] as [check.Absolute].
AbsUsrBin = unsafeAbs(UsrBin)
// AbsVar is [Var] as [check.Absolute].
AbsVar = unsafeAbs(Var)
// AbsVarLib is [VarLib] as [check.Absolute].
AbsVarLib = unsafeAbs(VarLib)
// AbsDev is [Dev] as [check.Absolute].
AbsDev = unsafeAbs(Dev)
// AbsProc is [Proc] as [check.Absolute].
AbsProc = unsafeAbs(Proc)
// AbsSys is [Sys] as [check.Absolute].
AbsSys = unsafeAbs(Sys)
)

View File

@ -1,38 +0,0 @@
// Package fhs provides constant and checked pathname values for common FHS paths.
package fhs
const (
// Root points to the file system root.
Root = "/"
// Etc points to the directory for system-specific configuration.
Etc = "/etc/"
// Tmp points to the place for small temporary files.
Tmp = "/tmp/"
// Run points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar.
Run = "/run/"
// RunUser points to a directory containing per-user runtime directories,
// each usually individually mounted "tmpfs" instances.
RunUser = Run + "user/"
// Usr points to vendor-supplied operating system resources.
Usr = "/usr/"
// UsrBin points to binaries and executables for user commands that shall appear in the $PATH search path.
UsrBin = Usr + "bin/"
// Var points to persistent, variable system data. Writable during normal system operation.
Var = "/var/"
// VarLib points to persistent system data.
VarLib = Var + "lib/"
// VarEmpty points to a nonstandard directory that is usually empty.
VarEmpty = Var + "empty/"
// Dev points to the root directory for device nodes.
Dev = "/dev/"
// Proc points to a virtual kernel file system exposing the process list and other functionality.
Proc = "/proc/"
// ProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
ProcSys = Proc + "sys/"
// Sys points to a virtual kernel file system exposing discovered devices and other functionality.
Sys = "/sys/"
)

View File

@ -3,7 +3,6 @@ package container
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"path"
@ -12,7 +11,6 @@ import (
. "syscall"
"time"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
)
@ -31,7 +29,7 @@ const (
it should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous */
intermediateHostPath = fhs.Proc + "self/fd"
intermediateHostPath = FHSProc + "self/fd"
// setup params file descriptor
setupEnv = "HAKUREI_SETUP"
@ -61,7 +59,6 @@ type (
setupState struct {
nonrepeatable uintptr
*Params
Msg
}
)
@ -94,23 +91,20 @@ type initParams struct {
Verbose bool
}
// Init is called by [TryArgv0] if the current process is the container init.
func Init(msg Msg) {
if msg == nil {
panic("attempting to call initEntrypoint with nil msg")
}
initEntrypoint(direct{}, msg)
func Init(prepareLogger func(prefix string), setVerbose func(verbose bool)) {
initEntrypoint(direct{}, prepareLogger, setVerbose)
}
func initEntrypoint(k syscallDispatcher, msg Msg) {
func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setVerbose func(verbose bool)) {
k.lockOSThread()
prepareLogger("init")
if k.getpid() != 1 {
k.fatal(msg, "this process must run as pid 1")
k.fatal("this process must run as pid 1")
}
if err := k.setPtracer(0); err != nil {
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
k.verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
// not fatal: this program has no additional privileges at initial program start
}
@ -122,65 +116,65 @@ func initEntrypoint(k syscallDispatcher, msg Msg) {
)
if f, err := k.receive(setupEnv, &params, &setupFd); err != nil {
if errors.Is(err, EBADF) {
k.fatal(msg, "invalid setup descriptor")
k.fatal("invalid setup descriptor")
}
if errors.Is(err, ErrReceiveEnv) {
k.fatal(msg, "HAKUREI_SETUP not set")
k.fatal("HAKUREI_SETUP not set")
}
k.fatalf(msg, "cannot decode init setup payload: %v", err)
k.fatalf("cannot decode init setup payload: %v", err)
} else {
if params.Ops == nil {
k.fatal(msg, "invalid setup parameters")
k.fatal("invalid setup parameters")
}
if params.ParentPerm == 0 {
params.ParentPerm = 0755
}
msg.SwapVerbose(params.Verbose)
msg.Verbose("received setup parameters")
setVerbose(params.Verbose)
k.verbose("received setup parameters")
closeSetup = f
offsetSetup = int(setupFd + 1)
}
// write uid/gid map here so parent does not need to set dumpable
if err := k.setDumpable(SUID_DUMP_USER); err != nil {
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
k.fatalf("cannot set SUID_DUMP_USER: %v", err)
}
if err := k.writeFile(fhs.Proc+"self/uid_map",
if err := k.writeFile(FHSProc+"self/uid_map",
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
0); err != nil {
k.fatalf(msg, "%v", err)
k.fatalf("%v", err)
}
if err := k.writeFile(fhs.Proc+"self/setgroups",
if err := k.writeFile(FHSProc+"self/setgroups",
[]byte("deny\n"),
0); err != nil && !os.IsNotExist(err) {
k.fatalf(msg, "%v", err)
k.fatalf("%v", err)
}
if err := k.writeFile(fhs.Proc+"self/gid_map",
if err := k.writeFile(FHSProc+"self/gid_map",
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
0); err != nil {
k.fatalf(msg, "%v", err)
k.fatalf("%v", err)
}
if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil {
k.fatalf(msg, "cannot set SUID_DUMP_DISABLE: %v", err)
k.fatalf("cannot set SUID_DUMP_DISABLE: %v", err)
}
oldmask := k.umask(0)
if params.Hostname != "" {
if err := k.sethostname([]byte(params.Hostname)); err != nil {
k.fatalf(msg, "cannot set hostname: %v", err)
k.fatalf("cannot set hostname: %v", err)
}
}
// cache sysctl before pivot_root
lastcap := k.lastcap(msg)
lastcap := k.lastcap()
if err := k.mount(zeroString, fhs.Root, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
k.fatalf(msg, "cannot make / rslave: %v", err)
if err := k.mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
k.fatalf("cannot make / rslave: %v", err)
}
state := &setupState{Params: &params.Params, Msg: msg}
state := &setupState{Params: &params.Params}
/* early is called right before pivot_root into intermediate root;
this step is mostly for gathering information that would otherwise be difficult to obtain
@ -188,41 +182,41 @@ func initEntrypoint(k syscallDispatcher, msg Msg) {
the state of the mount namespace */
for i, op := range *params.Ops {
if op == nil || !op.Valid() {
k.fatalf(msg, "invalid op at index %d", i)
k.fatalf("invalid op at index %d", i)
}
if err := op.early(state, k); err != nil {
if m, ok := messageFromError(err); ok {
k.fatal(msg, m)
k.fatal(m)
} else {
k.fatalf(msg, "cannot prepare op at index %d: %v", i, err)
k.fatalf("cannot prepare op at index %d: %v", i, err)
}
}
}
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
k.fatalf(msg, "cannot mount intermediate root: %v", err)
k.fatalf("cannot mount intermediate root: %v", err)
}
if err := k.chdir(intermediateHostPath); err != nil {
k.fatalf(msg, "cannot enter intermediate host path: %v", err)
k.fatalf("cannot enter intermediate host path: %v", err)
}
if err := k.mkdir(sysrootDir, 0755); err != nil {
k.fatalf(msg, "%v", err)
k.fatalf("%v", err)
}
if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil {
k.fatalf(msg, "cannot bind sysroot: %v", err)
k.fatalf("cannot bind sysroot: %v", err)
}
if err := k.mkdir(hostDir, 0755); err != nil {
k.fatalf(msg, "%v", err)
k.fatalf("%v", err)
}
// pivot_root uncovers intermediateHostPath in hostDir
if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil {
k.fatalf(msg, "cannot pivot into intermediate root: %v", err)
k.fatalf("cannot pivot into intermediate root: %v", err)
}
if err := k.chdir(fhs.Root); err != nil {
k.fatalf(msg, "cannot enter intermediate root: %v", err)
if err := k.chdir(FHSRoot); err != nil {
k.fatalf("cannot enter intermediate root: %v", err)
}
/* apply is called right after pivot_root and entering the new root;
@ -232,64 +226,64 @@ func initEntrypoint(k syscallDispatcher, msg Msg) {
for i, op := range *params.Ops {
// ops already checked during early setup
if prefix, ok := op.prefix(); ok {
msg.Verbosef("%s %s", prefix, op)
k.verbosef("%s %s", prefix, op)
}
if err := op.apply(state, k); err != nil {
if m, ok := messageFromError(err); ok {
k.fatal(msg, m)
k.fatal(m)
} else {
k.fatalf(msg, "cannot apply op at index %d: %v", i, err)
k.fatalf("cannot apply op at index %d: %v", i, err)
}
}
}
// setup requiring host root complete at this point
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
k.fatalf(msg, "cannot make host root rprivate: %v", err)
k.fatalf("cannot make host root rprivate: %v", err)
}
if err := k.unmount(hostDir, MNT_DETACH); err != nil {
k.fatalf(msg, "cannot unmount host root: %v", err)
k.fatalf("cannot unmount host root: %v", err)
}
{
var fd int
if err := IgnoringEINTR(func() (err error) {
fd, err = k.open(fhs.Root, O_DIRECTORY|O_RDONLY, 0)
fd, err = k.open(FHSRoot, O_DIRECTORY|O_RDONLY, 0)
return
}); err != nil {
k.fatalf(msg, "cannot open intermediate root: %v", err)
k.fatalf("cannot open intermediate root: %v", err)
}
if err := k.chdir(sysrootPath); err != nil {
k.fatalf(msg, "cannot enter sysroot: %v", err)
k.fatalf("cannot enter sysroot: %v", err)
}
if err := k.pivotRoot(".", "."); err != nil {
k.fatalf(msg, "cannot pivot into sysroot: %v", err)
k.fatalf("cannot pivot into sysroot: %v", err)
}
if err := k.fchdir(fd); err != nil {
k.fatalf(msg, "cannot re-enter intermediate root: %v", err)
k.fatalf("cannot re-enter intermediate root: %v", err)
}
if err := k.unmount(".", MNT_DETACH); err != nil {
k.fatalf(msg, "cannot unmount intermediate root: %v", err)
k.fatalf("cannot unmount intermediate root: %v", err)
}
if err := k.chdir(fhs.Root); err != nil {
k.fatalf(msg, "cannot enter root: %v", err)
if err := k.chdir(FHSRoot); err != nil {
k.fatalf("cannot enter root: %v", err)
}
if err := k.close(fd); err != nil {
k.fatalf(msg, "cannot close intermediate root: %v", err)
k.fatalf("cannot close intermediate root: %v", err)
}
}
if err := k.capAmbientClearAll(); err != nil {
k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
k.fatalf("cannot clear the ambient capability set: %v", err)
}
for i := uintptr(0); i <= lastcap; i++ {
if params.Privileged && i == CAP_SYS_ADMIN {
continue
}
if err := k.capBoundingSetDrop(i); err != nil {
k.fatalf(msg, "cannot drop capability from bounding set: %v", err)
k.fatalf("cannot drop capability from bounding set: %v", err)
}
}
@ -298,29 +292,29 @@ func initEntrypoint(k syscallDispatcher, msg Msg) {
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
k.fatalf(msg, "cannot raise CAP_SYS_ADMIN: %v", err)
k.fatalf("cannot raise CAP_SYS_ADMIN: %v", err)
}
}
if err := k.capset(
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
); err != nil {
k.fatalf(msg, "cannot capset: %v", err)
k.fatalf("cannot capset: %v", err)
}
if !params.SeccompDisable {
rules := params.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
k.verbosef("resolving presets %#x", params.SeccompPresets)
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
}
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
k.fatalf(msg, "cannot load syscall filter: %v", err)
k.fatalf("cannot load syscall filter: %v", err)
}
msg.Verbosef("%d filter rules loaded", len(rules))
k.verbosef("%d filter rules loaded", len(rules))
} else {
msg.Verbose("syscall filter not configured")
k.verbose("syscall filter not configured")
}
extraFiles := make([]*os.File, params.Count)
@ -337,14 +331,14 @@ func initEntrypoint(k syscallDispatcher, msg Msg) {
cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir.String()
msg.Verbosef("starting initial program %s", params.Path)
k.verbosef("starting initial program %s", params.Path)
if err := k.start(cmd); err != nil {
k.fatalf(msg, "%v", err)
k.fatalf("%v", err)
}
msg.Suspend()
k.suspend()
if err := closeSetup(); err != nil {
k.printf(msg, "cannot close setup pipe: %v", err)
k.printf("cannot close setup pipe: %v", err)
// not fatal
}
@ -378,7 +372,7 @@ func initEntrypoint(k syscallDispatcher, msg Msg) {
}
}
if !errors.Is(err, ECHILD) {
k.printf(msg, "unexpected wait4 response: %v", err)
k.printf("unexpected wait4 response: %v", err)
}
close(done)
@ -395,50 +389,50 @@ func initEntrypoint(k syscallDispatcher, msg Msg) {
for {
select {
case s := <-sig:
if msg.Resume() {
msg.Verbosef("%s after process start", s.String())
if k.resume() {
k.verbosef("%s after process start", s.String())
} else {
msg.Verbosef("got %s", s.String())
k.verbosef("got %s", s.String())
}
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
msg.Verbose("forwarding context cancellation")
k.verbose("forwarding context cancellation")
if err := k.signal(cmd, os.Interrupt); err != nil {
k.printf(msg, "cannot forward cancellation: %v", err)
k.printf("cannot forward cancellation: %v", err)
}
continue
}
msg.BeforeExit()
k.beforeExit()
k.exit(0)
case w := <-info:
if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
msg.Resume()
k.resume()
switch {
case w.wstatus.Exited():
r = w.wstatus.ExitStatus()
msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
k.verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal())
msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
k.verbosef("initial process exited with signal %s", w.wstatus.Signal())
default:
r = 255
msg.Verbosef("initial process exited with status %#x", w.wstatus)
k.verbosef("initial process exited with status %#x", w.wstatus)
}
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
}
case <-done:
msg.BeforeExit()
k.beforeExit()
k.exit(r)
case <-timeout:
k.printf(msg, "timeout exceeded waiting for lingering processes")
msg.BeforeExit()
k.printf("timeout exceeded waiting for lingering processes")
k.beforeExit()
k.exit(r)
}
}
@ -447,16 +441,10 @@ func initEntrypoint(k syscallDispatcher, msg Msg) {
const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init".
// If a nil msg is passed, the system logger is used instead.
func TryArgv0(msg Msg) {
if msg == nil {
log.SetPrefix(initName + ": ")
log.SetFlags(0)
msg = NewMsg(log.Default())
}
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
Init(msg)
msg = v
Init(prepare, setVerbose)
msg.BeforeExit()
os.Exit(0)
}

File diff suppressed because it is too large Load Diff

View File

@ -5,15 +5,12 @@ import (
"fmt"
"os"
"syscall"
"hakurei.app/container/bits"
"hakurei.app/container/check"
)
func init() { gob.Register(new(BindMountOp)) }
// Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target].
func (f *Ops) Bind(source, target *check.Absolute, flags int) *Ops {
func (f *Ops) Bind(source, target *Absolute, flags int) *Ops {
*f = append(*f, &BindMountOp{nil, source, target, flags})
return f
}
@ -21,39 +18,50 @@ func (f *Ops) Bind(source, target *check.Absolute, flags int) *Ops {
// BindMountOp bind mounts host path Source on container path Target.
// Note that Flags uses bits declared in this package and should not be set with constants in [syscall].
type BindMountOp struct {
sourceFinal, Source, Target *check.Absolute
sourceFinal, Source, Target *Absolute
Flags int
}
const (
// BindOptional skips nonexistent host paths.
BindOptional = 1 << iota
// BindWritable mounts filesystem read-write.
BindWritable
// BindDevice allows access to devices (special files) on this filesystem.
BindDevice
// BindEnsure attempts to create the host path if it does not exist.
BindEnsure
)
func (b *BindMountOp) Valid() bool {
return b != nil &&
b.Source != nil && b.Target != nil &&
b.Flags&(bits.BindOptional|bits.BindEnsure) != (bits.BindOptional|bits.BindEnsure)
b.Flags&(BindOptional|BindEnsure) != (BindOptional|BindEnsure)
}
func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
if b.Flags&bits.BindEnsure != 0 {
if b.Flags&BindEnsure != 0 {
if err := k.mkdirAll(b.Source.String(), 0700); err != nil {
return err
}
}
if pathname, err := k.evalSymlinks(b.Source.String()); err != nil {
if os.IsNotExist(err) && b.Flags&bits.BindOptional != 0 {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
// leave sourceFinal as nil
return nil
}
return err
} else {
b.sourceFinal, err = check.NewAbs(pathname)
b.sourceFinal, err = NewAbs(pathname)
return err
}
}
func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error {
func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
if b.sourceFinal == nil {
if b.Flags&bits.BindOptional == 0 {
if b.Flags&BindOptional == 0 {
// unreachable
return OpStateError("bind")
}
@ -76,19 +84,19 @@ func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error {
}
var flags uintptr = syscall.MS_REC
if b.Flags&bits.BindWritable == 0 {
if b.Flags&BindWritable == 0 {
flags |= syscall.MS_RDONLY
}
if b.Flags&bits.BindDevice == 0 {
if b.Flags&BindDevice == 0 {
flags |= syscall.MS_NODEV
}
if b.sourceFinal.String() == b.Target.String() {
state.Verbosef("mounting %q flags %#x", target, flags)
k.verbosef("mounting %q flags %#x", target, flags)
} else {
state.Verbosef("mounting %q on %q flags %#x", source, target, flags)
k.verbosef("mounting %q on %q flags %#x", source, target, flags)
}
return k.bindMount(state, source, target, flags)
return k.bindMount(source, target, flags)
}
func (b *BindMountOp) Is(op Op) bool {

View File

@ -6,32 +6,30 @@ import (
"syscall"
"testing"
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestBindMountOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"ENOENT not optional", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
}, syscall.ENOENT, nil, nil},
{"skip optional", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Flags: bits.BindOptional,
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
Flags: BindOptional,
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
}, nil, nil, nil},
{"success optional", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Flags: bits.BindOptional,
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
Flags: BindOptional,
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{
@ -42,9 +40,9 @@ func TestBindMountOp(t *testing.T) {
}, nil},
{"ensureFile device", new(Params), &BindMountOp{
Source: check.MustAbs("/dev/null"),
Target: check.MustAbs("/dev/null"),
Flags: bits.BindWritable | bits.BindDevice,
Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"),
Flags: BindWritable | BindDevice,
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []stub.Call{
@ -53,17 +51,17 @@ func TestBindMountOp(t *testing.T) {
}, stub.UniqueError(5)},
{"mkdirAll ensure", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Flags: bits.BindEnsure,
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
Flags: BindEnsure,
}, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, stub.UniqueError(4)),
}, stub.UniqueError(4), nil, nil},
{"success ensure", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/usr/bin/"),
Flags: bits.BindEnsure,
Source: MustAbs("/bin/"),
Target: MustAbs("/usr/bin/"),
Flags: BindEnsure,
}, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
@ -75,9 +73,9 @@ func TestBindMountOp(t *testing.T) {
}, nil},
{"success device ro", new(Params), &BindMountOp{
Source: check.MustAbs("/dev/null"),
Target: check.MustAbs("/dev/null"),
Flags: bits.BindDevice,
Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"),
Flags: BindDevice,
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []stub.Call{
@ -88,9 +86,9 @@ func TestBindMountOp(t *testing.T) {
}, nil},
{"success device", new(Params), &BindMountOp{
Source: check.MustAbs("/dev/null"),
Target: check.MustAbs("/dev/null"),
Flags: bits.BindWritable | bits.BindDevice,
Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"),
Flags: BindWritable | BindDevice,
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []stub.Call{
@ -101,15 +99,15 @@ func TestBindMountOp(t *testing.T) {
}, nil},
{"evalSymlinks", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", stub.UniqueError(3)),
}, stub.UniqueError(3), nil, nil},
{"stat", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{
@ -117,8 +115,8 @@ func TestBindMountOp(t *testing.T) {
}, stub.UniqueError(2)},
{"mkdirAll", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{
@ -127,8 +125,8 @@ func TestBindMountOp(t *testing.T) {
}, stub.UniqueError(1)},
{"bindMount", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{
@ -139,8 +137,8 @@ func TestBindMountOp(t *testing.T) {
}, stub.UniqueError(0)},
{"success eval equals", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/bin", nil),
}, nil, []stub.Call{
@ -151,8 +149,8 @@ func TestBindMountOp(t *testing.T) {
}, nil},
{"success", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{
@ -175,21 +173,21 @@ func TestBindMountOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*BindMountOp)(nil), false},
{"zero", new(BindMountOp), false},
{"nil source", &BindMountOp{Target: check.MustAbs("/")}, false},
{"nil target", &BindMountOp{Source: check.MustAbs("/")}, false},
{"flag optional ensure", &BindMountOp{Source: check.MustAbs("/"), Target: check.MustAbs("/"), Flags: bits.BindOptional | bits.BindEnsure}, false},
{"valid", &BindMountOp{Source: check.MustAbs("/"), Target: check.MustAbs("/")}, true},
{"nil source", &BindMountOp{Target: MustAbs("/")}, false},
{"nil target", &BindMountOp{Source: MustAbs("/")}, false},
{"flag optional ensure", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindOptional | BindEnsure}, false},
{"valid", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"autoetc", new(Ops).Bind(
check.MustAbs("/etc/"),
check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
MustAbs("/etc/"),
MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
0,
), Ops{
&BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
},
}},
})
@ -198,45 +196,45 @@ func TestBindMountOp(t *testing.T) {
{"zero", new(BindMountOp), new(BindMountOp), false},
{"internal ne", &BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
sourceFinal: check.MustAbs("/etc/"),
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
sourceFinal: MustAbs("/etc/"),
}, true},
{"flags differs", &BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Flags: bits.BindOptional,
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Flags: BindOptional,
}, false},
{"source differs", &BindMountOp{
Source: check.MustAbs("/.hakurei/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: MustAbs("/.hakurei/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, false},
{"target differs", &BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/"),
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/"),
}, false},
{"equals", &BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, true},
})
@ -244,14 +242,14 @@ func TestBindMountOp(t *testing.T) {
{"invalid", new(BindMountOp), "mounting", "<invalid>"},
{"autoetc", &BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, "mounting", `"/etc/" on "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659" flags 0x0`},
{"hostdev", &BindMountOp{
Source: check.MustAbs("/dev/"),
Target: check.MustAbs("/dev/"),
Flags: bits.BindWritable | bits.BindDevice,
Source: MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Flags: BindWritable | BindDevice,
}, "mounting", `"/dev/" flags 0x6`},
})
}

View File

@ -5,22 +5,19 @@ import (
"fmt"
"path"
. "syscall"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
func init() { gob.Register(new(MountDevOp)) }
// Dev appends an [Op] that mounts a subset of host /dev.
func (f *Ops) Dev(target *check.Absolute, mqueue bool) *Ops {
func (f *Ops) Dev(target *Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{target, mqueue, false})
return f
}
// DevWritable appends an [Op] that mounts a writable subset of host /dev.
// There is usually no good reason to write to /dev, so this should always be followed by a [RemountOp].
func (f *Ops) DevWritable(target *check.Absolute, mqueue bool) *Ops {
func (f *Ops) DevWritable(target *Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{target, mqueue, true})
return f
}
@ -29,7 +26,7 @@ func (f *Ops) DevWritable(target *check.Absolute, mqueue bool) *Ops {
// If Mqueue is true, a private instance of [FstypeMqueue] is mounted.
// If Write is true, the resulting mount point is left writable.
type MountDevOp struct {
Target *check.Absolute
Target *Absolute
Mqueue bool
Write bool
}
@ -49,8 +46,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
return err
}
if err := k.bindMount(
state,
toHost(fhs.Dev+name),
toHost(FHSDev+name),
targetPath,
0,
); err != nil {
@ -59,15 +55,15 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
}
for i, name := range []string{"stdin", "stdout", "stderr"} {
if err := k.symlink(
fhs.Proc+"self/fd/"+string(rune(i+'0')),
FHSProc+"self/fd/"+string(rune(i+'0')),
path.Join(target, name),
); err != nil {
return err
}
}
for _, pair := range [][2]string{
{fhs.Proc + "self/fd", "fd"},
{fhs.Proc + "kcore", "core"},
{FHSProc + "self/fd", "fd"},
{FHSProc + "kcore", "core"},
{"pts/ptmx", "ptmx"},
} {
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
@ -97,7 +93,6 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
if name, err := k.readlink(hostProc.stdout()); err != nil {
return err
} else if err = k.bindMount(
state,
toHost(name),
consolePath,
0,
@ -121,7 +116,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
return nil
}
if err := k.remount(state, target, MS_RDONLY); err != nil {
if err := k.remount(target, MS_RDONLY); err != nil {
return err
}
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)

View File

@ -4,21 +4,20 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestMountDevOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"mountTmpfs", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, stub.UniqueError(27)),
}, stub.UniqueError(27)},
{"ensureFile null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -26,7 +25,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(26)},
{"bindMount null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -35,7 +34,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(25)},
{"ensureFile zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -45,7 +44,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(24)},
{"bindMount zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -56,7 +55,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(23)},
{"ensureFile full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -68,7 +67,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(22)},
{"bindMount full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -81,7 +80,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(21)},
{"ensureFile random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -95,7 +94,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(20)},
{"bindMount random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -110,7 +109,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(19)},
{"ensureFile urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -126,7 +125,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(18)},
{"bindMount urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -143,7 +142,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(17)},
{"ensureFile tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -161,7 +160,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(16)},
{"bindMount tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -180,7 +179,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(15)},
{"symlink stdin", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -200,7 +199,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(14)},
{"symlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -221,7 +220,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(13)},
{"symlink stderr", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -243,7 +242,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(12)},
{"symlink fd", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -266,7 +265,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(11)},
{"symlink kcore", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -290,7 +289,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(10)},
{"symlink ptmx", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -315,7 +314,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(9)},
{"mkdir shm", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -341,7 +340,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(8)},
{"mkdir devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -368,7 +367,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(7)},
{"mount devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -396,7 +395,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(6)},
{"ensureFile stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -426,7 +425,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(5)},
{"readlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -457,7 +456,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(4)},
{"bindMount stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -489,7 +488,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(3)},
{"mkdir mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -522,7 +521,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(2)},
{"mount mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -556,7 +555,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(1)},
{"success no session", &Params{ParentPerm: 0755}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
Write: true,
}, nil, nil, []stub.Call{
@ -587,7 +586,7 @@ func TestMountDevOp(t *testing.T) {
}, nil},
{"success no tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
Write: true,
}, nil, nil, []stub.Call{
@ -619,7 +618,7 @@ func TestMountDevOp(t *testing.T) {
}, nil},
{"remount", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
@ -651,7 +650,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(0)},
{"success no mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
@ -684,7 +683,7 @@ func TestMountDevOp(t *testing.T) {
}, nil},
{"success rw", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
Write: true,
}, nil, nil, []stub.Call{
@ -719,7 +718,7 @@ func TestMountDevOp(t *testing.T) {
}, nil},
{"success", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@ -758,20 +757,20 @@ func TestMountDevOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountDevOp)(nil), false},
{"zero", new(MountDevOp), false},
{"valid", &MountDevOp{Target: check.MustAbs("/dev/")}, true},
{"valid", &MountDevOp{Target: MustAbs("/dev/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"dev", new(Ops).Dev(check.MustAbs("/dev/"), true), Ops{
{"dev", new(Ops).Dev(MustAbs("/dev/"), true), Ops{
&MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
},
}},
{"dev writable", new(Ops).DevWritable(check.MustAbs("/.hakurei/dev/"), false), Ops{
{"dev writable", new(Ops).DevWritable(MustAbs("/.hakurei/dev/"), false), Ops{
&MountDevOp{
Target: check.MustAbs("/.hakurei/dev/"),
Target: MustAbs("/.hakurei/dev/"),
Write: true,
},
}},
@ -781,46 +780,46 @@ func TestMountDevOp(t *testing.T) {
{"zero", new(MountDevOp), new(MountDevOp), false},
{"write differs", &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
Write: true,
}, false},
{"mqueue differs", &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, false},
{"target differs", &MountDevOp{
Target: check.MustAbs("/"),
Target: MustAbs("/"),
Mqueue: true,
}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, false},
{"equals", &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"mqueue", &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, "mounting", `dev on "/dev/" with mqueue`},
{"dev", &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
}, "mounting", `dev on "/dev/"`},
})
}

View File

@ -4,21 +4,19 @@ import (
"encoding/gob"
"fmt"
"os"
"hakurei.app/container/check"
)
func init() { gob.Register(new(MkdirOp)) }
// Mkdir appends an [Op] that creates a directory in the container filesystem.
func (f *Ops) Mkdir(name *check.Absolute, perm os.FileMode) *Ops {
func (f *Ops) Mkdir(name *Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{name, perm})
return f
}
// MkdirOp creates a directory at container Path with permission bits set to Perm.
type MkdirOp struct {
Path *check.Absolute
Path *Absolute
Perm os.FileMode
}

View File

@ -4,14 +4,13 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestMkdirOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"success", new(Params), &MkdirOp{
Path: check.MustAbs("/.hakurei"),
Path: MustAbs("/.hakurei"),
Perm: 0500,
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/.hakurei", os.FileMode(0500)}, nil, nil),
@ -21,25 +20,25 @@ func TestMkdirOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*MkdirOp)(nil), false},
{"zero", new(MkdirOp), false},
{"valid", &MkdirOp{Path: check.MustAbs("/.hakurei")}, true},
{"valid", &MkdirOp{Path: MustAbs("/.hakurei")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"etc", new(Ops).Mkdir(check.MustAbs("/etc/"), 0), Ops{
&MkdirOp{Path: check.MustAbs("/etc/")},
{"etc", new(Ops).Mkdir(MustAbs("/etc/"), 0), Ops{
&MkdirOp{Path: MustAbs("/etc/")},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(MkdirOp), new(MkdirOp), false},
{"path differs", &MkdirOp{Path: check.MustAbs("/"), Perm: 0755}, &MkdirOp{Path: check.MustAbs("/etc/"), Perm: 0755}, false},
{"perm differs", &MkdirOp{Path: check.MustAbs("/")}, &MkdirOp{Path: check.MustAbs("/"), Perm: 0755}, false},
{"equals", &MkdirOp{Path: check.MustAbs("/")}, &MkdirOp{Path: check.MustAbs("/")}, true},
{"path differs", &MkdirOp{Path: MustAbs("/"), Perm: 0755}, &MkdirOp{Path: MustAbs("/etc/"), Perm: 0755}, false},
{"perm differs", &MkdirOp{Path: MustAbs("/")}, &MkdirOp{Path: MustAbs("/"), Perm: 0755}, false},
{"equals", &MkdirOp{Path: MustAbs("/")}, &MkdirOp{Path: MustAbs("/")}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"etc", &MkdirOp{
Path: check.MustAbs("/etc/"),
Path: MustAbs("/etc/"),
}, "creating", `directory "/etc/" perm ----------`},
})
}

View File

@ -5,9 +5,6 @@ import (
"fmt"
"slices"
"strings"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
const (
@ -55,7 +52,7 @@ func (e *OverlayArgumentError) Error() string {
}
// Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target].
func (f *Ops) Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) *Ops {
func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops {
*f = append(*f, &MountOverlayOp{
Target: target,
Lower: layers,
@ -67,34 +64,34 @@ func (f *Ops) Overlay(target, state, work *check.Absolute, layers ...*check.Abso
// OverlayEphemeral appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]
// with an ephemeral upperdir and workdir.
func (f *Ops) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) *Ops {
return f.Overlay(target, fhs.AbsRoot, nil, layers...)
func (f *Ops) OverlayEphemeral(target *Absolute, layers ...*Absolute) *Ops {
return f.Overlay(target, AbsFHSRoot, nil, layers...)
}
// OverlayReadonly appends an [Op] that mounts the overlay pseudo filesystem readonly on [MountOverlayOp.Target]
func (f *Ops) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) *Ops {
func (f *Ops) OverlayReadonly(target *Absolute, layers ...*Absolute) *Ops {
return f.Overlay(target, nil, nil, layers...)
}
// MountOverlayOp mounts [FstypeOverlay] on container path Target.
type MountOverlayOp struct {
Target *check.Absolute
Target *Absolute
// Any filesystem, does not need to be on a writable filesystem.
Lower []*check.Absolute
Lower []*Absolute
// formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early
lower []string
// The upperdir is normally on a writable filesystem.
//
// If Work is nil and Upper holds the special value [fhs.AbsRoot],
// If Work is nil and Upper holds the special value [AbsFHSRoot],
// an ephemeral upperdir and workdir will be set up.
//
// If both Work and Upper are nil, upperdir and workdir is omitted and the overlay is mounted readonly.
Upper *check.Absolute
Upper *Absolute
// formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early
upper string
// The workdir needs to be an empty directory on the same filesystem as upperdir.
Work *check.Absolute
Work *Absolute
// formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early
work string
@ -120,7 +117,7 @@ func (o *MountOverlayOp) Valid() bool {
func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if o.Work == nil && o.Upper != nil {
switch o.Upper.String() {
case fhs.Root: // ephemeral
case FHSRoot: // ephemeral
o.ephemeral = true // intermediate root not yet available
default:
@ -139,7 +136,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
return err
} else {
o.upper = check.EscapeOverlayDataSegment(toHost(v))
o.upper = EscapeOverlayDataSegment(toHost(v))
}
}
@ -147,7 +144,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
return err
} else {
o.work = check.EscapeOverlayDataSegment(toHost(v))
o.work = EscapeOverlayDataSegment(toHost(v))
}
}
}
@ -157,7 +154,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if v, err := k.evalSymlinks(a.String()); err != nil {
return err
} else {
o.lower[i] = check.EscapeOverlayDataSegment(toHost(v))
o.lower[i] = EscapeOverlayDataSegment(toHost(v))
}
}
return nil
@ -175,10 +172,10 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
if o.ephemeral {
var err error
// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
if o.upper, err = k.mkdirTemp(fhs.Root, intermediatePatternOverlayUpper); err != nil {
if o.upper, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil {
return err
}
if o.work, err = k.mkdirTemp(fhs.Root, intermediatePatternOverlayWork); err != nil {
if o.work, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil {
return err
}
}
@ -199,17 +196,17 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
OptionOverlayWorkdir+"="+o.work)
}
options = append(options,
OptionOverlayLowerdir+"="+strings.Join(o.lower, check.SpecialOverlayPath),
OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath),
OptionOverlayUserxattr)
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption))
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption))
}
func (o *MountOverlayOp) Is(op Op) bool {
vo, ok := op.(*MountOverlayOp)
return ok && o.Valid() && vo.Valid() &&
o.Target.Is(vo.Target) &&
slices.EqualFunc(o.Lower, vo.Lower, func(a, v *check.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)
}
func (*MountOverlayOp) prefix() (string, bool) { return "mounting", true }

View File

@ -5,7 +5,6 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
@ -39,21 +38,21 @@ func TestMountOverlayOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdirTemp invalid ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: check.MustAbs("/"),
Lower: []*check.Absolute{
check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
check.MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: check.MustAbs("/proc/"),
Upper: MustAbs("/proc/"),
}, nil, &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, "/proc/"}, nil, nil},
{"mkdirTemp upper ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: check.MustAbs("/"),
Lower: []*check.Absolute{
check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
check.MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: check.MustAbs("/"),
Upper: MustAbs("/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
@ -63,12 +62,12 @@ func TestMountOverlayOp(t *testing.T) {
}, stub.UniqueError(6)},
{"mkdirTemp work ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: check.MustAbs("/"),
Lower: []*check.Absolute{
check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
check.MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: check.MustAbs("/"),
Upper: MustAbs("/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
@ -79,12 +78,12 @@ func TestMountOverlayOp(t *testing.T) {
}, stub.UniqueError(5)},
{"success ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: check.MustAbs("/"),
Lower: []*check.Absolute{
check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
check.MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: check.MustAbs("/"),
Upper: MustAbs("/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
@ -102,9 +101,9 @@ func TestMountOverlayOp(t *testing.T) {
}, nil},
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{
check.MustAbs("/mnt-root/nix/.ro-store"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
},
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
@ -113,10 +112,10 @@ func TestMountOverlayOp(t *testing.T) {
}, &OverlayArgumentError{OverlayReadonlyLower, zeroString}},
{"success ro noPrefix", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{
check.MustAbs("/mnt-root/nix/.ro-store"),
check.MustAbs("/mnt-root/nix/.ro-store0"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"),
},
noPrefix: true,
}, []stub.Call{
@ -132,10 +131,10 @@ func TestMountOverlayOp(t *testing.T) {
}, nil},
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{
check.MustAbs("/mnt-root/nix/.ro-store"),
check.MustAbs("/mnt-root/nix/.ro-store0"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"),
},
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
@ -150,9 +149,9 @@ func TestMountOverlayOp(t *testing.T) {
}, nil},
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
@ -161,29 +160,29 @@ func TestMountOverlayOp(t *testing.T) {
}, &OverlayArgumentError{OverlayEmptyLower, zeroString}},
{"evalSymlinks upper", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", stub.UniqueError(4)),
}, stub.UniqueError(4), nil, nil},
{"evalSymlinks work", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", stub.UniqueError(3)),
}, stub.UniqueError(3), nil, nil},
{"evalSymlinks lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
@ -191,10 +190,10 @@ func TestMountOverlayOp(t *testing.T) {
}, stub.UniqueError(2), nil, nil},
{"mkdirAll", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
@ -204,10 +203,10 @@ func TestMountOverlayOp(t *testing.T) {
}, stub.UniqueError(1)},
{"mount", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
@ -218,10 +217,10 @@ func TestMountOverlayOp(t *testing.T) {
}, stub.UniqueError(0)},
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
@ -236,16 +235,16 @@ func TestMountOverlayOp(t *testing.T) {
}, nil},
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{
check.MustAbs("/mnt-root/nix/.ro-store"),
check.MustAbs("/mnt-root/nix/.ro-store0"),
check.MustAbs("/mnt-root/nix/.ro-store1"),
check.MustAbs("/mnt-root/nix/.ro-store2"),
check.MustAbs("/mnt-root/nix/.ro-store3"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"),
MustAbs("/mnt-root/nix/.ro-store1"),
MustAbs("/mnt-root/nix/.ro-store2"),
MustAbs("/mnt-root/nix/.ro-store3"),
},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
@ -273,7 +272,7 @@ func TestMountOverlayOp(t *testing.T) {
t.Run("nil Upper non-nil Work not ephemeral", func(t *testing.T) {
wantErr := OpStateError("overlay")
if err := (&MountOverlayOp{
Work: check.MustAbs("/"),
Work: MustAbs("/"),
}).early(nil, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
}
@ -283,39 +282,39 @@ func TestMountOverlayOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountOverlayOp)(nil), false},
{"zero", new(MountOverlayOp), false},
{"nil lower", &MountOverlayOp{Target: check.MustAbs("/"), Lower: []*check.Absolute{nil}}, false},
{"ro", &MountOverlayOp{Target: check.MustAbs("/"), Lower: []*check.Absolute{check.MustAbs("/")}}, true},
{"ro work", &MountOverlayOp{Target: check.MustAbs("/"), Work: check.MustAbs("/tmp/")}, false},
{"rw", &MountOverlayOp{Target: check.MustAbs("/"), Lower: []*check.Absolute{check.MustAbs("/")}, Upper: check.MustAbs("/"), Work: check.MustAbs("/")}, true},
{"nil lower", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{nil}}, false},
{"ro", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{MustAbs("/")}}, true},
{"ro work", &MountOverlayOp{Target: MustAbs("/"), Work: MustAbs("/tmp/")}, false},
{"rw", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{MustAbs("/")}, Upper: MustAbs("/"), Work: MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"full", new(Ops).Overlay(
check.MustAbs("/nix/store"),
check.MustAbs("/mnt-root/nix/.rw-store/upper"),
check.MustAbs("/mnt-root/nix/.rw-store/work"),
check.MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/nix/store"),
MustAbs("/mnt-root/nix/.rw-store/upper"),
MustAbs("/mnt-root/nix/.rw-store/work"),
MustAbs("/mnt-root/nix/.ro-store"),
), Ops{
&MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
},
}},
{"ephemeral", new(Ops).OverlayEphemeral(check.MustAbs("/nix/store"), check.MustAbs("/mnt-root/nix/.ro-store")), Ops{
{"ephemeral", new(Ops).OverlayEphemeral(MustAbs("/nix/store"), MustAbs("/mnt-root/nix/.ro-store")), Ops{
&MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/"),
},
}},
{"readonly", new(Ops).OverlayReadonly(check.MustAbs("/nix/store"), check.MustAbs("/mnt-root/nix/.ro-store")), Ops{
{"readonly", new(Ops).OverlayReadonly(MustAbs("/nix/store"), MustAbs("/mnt-root/nix/.ro-store")), Ops{
&MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
},
}},
})
@ -324,74 +323,74 @@ func TestMountOverlayOp(t *testing.T) {
{"zero", new(MountOverlayOp), new(MountOverlayOp), false},
{"differs target", &MountOverlayOp{
Target: check.MustAbs("/nix/store/differs"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store/differs"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, false},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"differs lower", &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store/differs")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store/differs")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, false},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"differs upper", &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper/differs"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper/differs"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, false},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"differs work", &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work/differs"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work/differs"),
}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, false},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"equals ro", &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")}}, true},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}}, true},
{"equals", &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, true},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"nix", &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, "mounting", `overlay on "/nix/store" with 1 layers`},
})
}

View File

@ -4,9 +4,6 @@ import (
"encoding/gob"
"fmt"
"syscall"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
const (
@ -17,13 +14,13 @@ const (
func init() { gob.Register(new(TmpfileOp)) }
// Place appends an [Op] that places a file in container path [TmpfileOp.Path] containing [TmpfileOp.Data].
func (f *Ops) Place(name *check.Absolute, data []byte) *Ops {
func (f *Ops) Place(name *Absolute, data []byte) *Ops {
*f = append(*f, &TmpfileOp{name, data})
return f
}
// PlaceP is like Place but writes the address of [TmpfileOp.Data] to the pointer dataP points to.
func (f *Ops) PlaceP(name *check.Absolute, dataP **[]byte) *Ops {
func (f *Ops) PlaceP(name *Absolute, dataP **[]byte) *Ops {
t := &TmpfileOp{Path: name}
*dataP = &t.Data
@ -33,7 +30,7 @@ func (f *Ops) PlaceP(name *check.Absolute, dataP **[]byte) *Ops {
// TmpfileOp places a file on container Path containing Data.
type TmpfileOp struct {
Path *check.Absolute
Path *Absolute
Data []byte
}
@ -41,7 +38,7 @@ func (t *TmpfileOp) Valid() bool { return t != ni
func (t *TmpfileOp) early(*setupState, syscallDispatcher) error { return nil }
func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
var tmpPath string
if f, err := k.createTemp(fhs.Root, intermediatePatternTmpfile); err != nil {
if f, err := k.createTemp(FHSRoot, intermediatePatternTmpfile); err != nil {
return err
} else if _, err = f.Write(t.Data); err != nil {
return err
@ -55,7 +52,6 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
if err := k.ensureFile(target, 0444, state.ParentPerm); err != nil {
return err
} else if err = k.bindMount(
state,
tmpPath,
target,
syscall.MS_RDONLY|syscall.MS_NODEV,

View File

@ -4,14 +4,13 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestTmpfileOp(t *testing.T) {
const sampleDataString = `chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`
var (
samplePath = check.MustAbs("/etc/passwd")
samplePath = MustAbs("/etc/passwd")
sampleData = []byte(sampleDataString)
)
@ -101,7 +100,7 @@ func TestTmpfileOp(t *testing.T) {
{"zero", new(TmpfileOp), new(TmpfileOp), false},
{"differs path", &TmpfileOp{
Path: check.MustAbs("/etc/group"),
Path: MustAbs("/etc/group"),
Data: sampleData,
}, &TmpfileOp{
Path: samplePath,

View File

@ -4,20 +4,18 @@ import (
"encoding/gob"
"fmt"
. "syscall"
"hakurei.app/container/check"
)
func init() { gob.Register(new(MountProcOp)) }
// Proc appends an [Op] that mounts a private instance of proc.
func (f *Ops) Proc(target *check.Absolute) *Ops {
func (f *Ops) Proc(target *Absolute) *Ops {
*f = append(*f, &MountProcOp{target})
return f
}
// MountProcOp mounts a new instance of [FstypeProc] on container path Target.
type MountProcOp struct{ Target *check.Absolute }
type MountProcOp struct{ Target *Absolute }
func (p *MountProcOp) Valid() bool { return p != nil && p.Target != nil }
func (p *MountProcOp) early(*setupState, syscallDispatcher) error { return nil }

View File

@ -4,7 +4,6 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
@ -12,14 +11,14 @@ func TestMountProcOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdir", &Params{ParentPerm: 0755},
&MountProcOp{
Target: check.MustAbs("/proc/"),
Target: MustAbs("/proc/"),
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, stub.UniqueError(0)),
}, stub.UniqueError(0)},
{"success", &Params{ParentPerm: 0700},
&MountProcOp{
Target: check.MustAbs("/proc/"),
Target: MustAbs("/proc/"),
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil),
call("mount", stub.ExpectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil),
@ -29,12 +28,12 @@ func TestMountProcOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountProcOp)(nil), false},
{"zero", new(MountProcOp), false},
{"valid", &MountProcOp{Target: check.MustAbs("/proc/")}, true},
{"valid", &MountProcOp{Target: MustAbs("/proc/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"proc", new(Ops).Proc(check.MustAbs("/proc/")), Ops{
&MountProcOp{Target: check.MustAbs("/proc/")},
{"proc", new(Ops).Proc(MustAbs("/proc/")), Ops{
&MountProcOp{Target: MustAbs("/proc/")},
}},
})
@ -42,20 +41,20 @@ func TestMountProcOp(t *testing.T) {
{"zero", new(MountProcOp), new(MountProcOp), false},
{"target differs", &MountProcOp{
Target: check.MustAbs("/proc/nonexistent"),
Target: MustAbs("/proc/nonexistent"),
}, &MountProcOp{
Target: check.MustAbs("/proc/"),
Target: MustAbs("/proc/"),
}, false},
{"equals", &MountProcOp{
Target: check.MustAbs("/proc/"),
Target: MustAbs("/proc/"),
}, &MountProcOp{
Target: check.MustAbs("/proc/"),
Target: MustAbs("/proc/"),
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"proc", &MountProcOp{Target: check.MustAbs("/proc/")},
{"proc", &MountProcOp{Target: MustAbs("/proc/")},
"mounting", `proc on "/proc/"`},
})
}

View File

@ -3,28 +3,26 @@ package container
import (
"encoding/gob"
"fmt"
"hakurei.app/container/check"
)
func init() { gob.Register(new(RemountOp)) }
// Remount appends an [Op] that applies [RemountOp.Flags] on container path [RemountOp.Target].
func (f *Ops) Remount(target *check.Absolute, flags uintptr) *Ops {
func (f *Ops) Remount(target *Absolute, flags uintptr) *Ops {
*f = append(*f, &RemountOp{target, flags})
return f
}
// RemountOp remounts Target with Flags.
type RemountOp struct {
Target *check.Absolute
Target *Absolute
Flags uintptr
}
func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil }
func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
func (r *RemountOp) apply(state *setupState, k syscallDispatcher) error {
return k.remount(state, toSysroot(r.Target.String()), r.Flags)
func (r *RemountOp) apply(_ *setupState, k syscallDispatcher) error {
return k.remount(toSysroot(r.Target.String()), r.Flags)
}
func (r *RemountOp) Is(op Op) bool {

View File

@ -4,14 +4,13 @@ import (
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestRemountOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"success", new(Params), &RemountOp{
Target: check.MustAbs("/"),
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, nil, nil, []stub.Call{
call("remount", stub.ExpectArgs{"/sysroot", uintptr(1)}, nil, nil),
@ -21,13 +20,13 @@ func TestRemountOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*RemountOp)(nil), false},
{"zero", new(RemountOp), false},
{"valid", &RemountOp{Target: check.MustAbs("/"), Flags: syscall.MS_RDONLY}, true},
{"valid", &RemountOp{Target: MustAbs("/"), Flags: syscall.MS_RDONLY}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"root", new(Ops).Remount(check.MustAbs("/"), syscall.MS_RDONLY), Ops{
{"root", new(Ops).Remount(MustAbs("/"), syscall.MS_RDONLY), Ops{
&RemountOp{
Target: check.MustAbs("/"),
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
},
}},
@ -37,33 +36,33 @@ func TestRemountOp(t *testing.T) {
{"zero", new(RemountOp), new(RemountOp), false},
{"target differs", &RemountOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Flags: syscall.MS_RDONLY,
}, &RemountOp{
Target: check.MustAbs("/"),
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, false},
{"flags differs", &RemountOp{
Target: check.MustAbs("/"),
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY | syscall.MS_NODEV,
}, &RemountOp{
Target: check.MustAbs("/"),
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, false},
{"equals", &RemountOp{
Target: check.MustAbs("/"),
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, &RemountOp{
Target: check.MustAbs("/"),
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"root", &RemountOp{
Target: check.MustAbs("/"),
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, "remounting", `"/" flags 0x1`},
})

View File

@ -4,21 +4,19 @@ import (
"encoding/gob"
"fmt"
"path"
"hakurei.app/container/check"
)
func init() { gob.Register(new(SymlinkOp)) }
// Link appends an [Op] that creates a symlink in the container filesystem.
func (f *Ops) Link(target *check.Absolute, linkName string, dereference bool) *Ops {
func (f *Ops) Link(target *Absolute, linkName string, dereference bool) *Ops {
*f = append(*f, &SymlinkOp{target, linkName, dereference})
return f
}
// SymlinkOp optionally dereferences LinkName and creates a symlink at container path Target.
type SymlinkOp struct {
Target *check.Absolute
Target *Absolute
// LinkName is an arbitrary uninterpreted pathname.
LinkName string
@ -30,8 +28,8 @@ func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkN
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
if l.Dereference {
if !path.IsAbs(l.LinkName) {
return &check.AbsoluteError{Pathname: l.LinkName}
if !isAbs(l.LinkName) {
return &AbsoluteError{l.LinkName}
}
if name, err := k.readlink(l.LinkName); err != nil {
return err

View File

@ -4,27 +4,26 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestSymlinkOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: check.MustAbs("/etc/nixos"),
Target: MustAbs("/etc/nixos"),
LinkName: "/etc/static/nixos",
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, stub.UniqueError(1)),
}, stub.UniqueError(1)},
{"abs", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: check.MustAbs("/etc/mtab"),
Target: MustAbs("/etc/mtab"),
LinkName: "etc/mtab",
Dereference: true,
}, nil, &check.AbsoluteError{Pathname: "etc/mtab"}, nil, nil},
}, nil, &AbsoluteError{"etc/mtab"}, nil, nil},
{"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: check.MustAbs("/etc/mtab"),
Target: MustAbs("/etc/mtab"),
LinkName: "/etc/mtab",
Dereference: true,
}, []stub.Call{
@ -32,7 +31,7 @@ func TestSymlinkOp(t *testing.T) {
}, stub.UniqueError(0), nil, nil},
{"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: check.MustAbs("/etc/nixos"),
Target: MustAbs("/etc/nixos"),
LinkName: "/etc/static/nixos",
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil),
@ -40,7 +39,7 @@ func TestSymlinkOp(t *testing.T) {
}, nil},
{"success", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: check.MustAbs("/etc/mtab"),
Target: MustAbs("/etc/mtab"),
LinkName: "/etc/mtab",
Dereference: true,
}, []stub.Call{
@ -55,18 +54,18 @@ func TestSymlinkOp(t *testing.T) {
{"nil", (*SymlinkOp)(nil), false},
{"zero", new(SymlinkOp), false},
{"nil target", &SymlinkOp{LinkName: "/run/current-system"}, false},
{"zero linkname", &SymlinkOp{Target: check.MustAbs("/run/current-system")}, false},
{"valid", &SymlinkOp{Target: check.MustAbs("/run/current-system"), LinkName: "/run/current-system", Dereference: true}, true},
{"zero linkname", &SymlinkOp{Target: MustAbs("/run/current-system")}, false},
{"valid", &SymlinkOp{Target: MustAbs("/run/current-system"), LinkName: "/run/current-system", Dereference: true}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"current-system", new(Ops).Link(
check.MustAbs("/run/current-system"),
MustAbs("/run/current-system"),
"/run/current-system",
true,
), Ops{
&SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
},
@ -77,40 +76,40 @@ func TestSymlinkOp(t *testing.T) {
{"zero", new(SymlinkOp), new(SymlinkOp), false},
{"target differs", &SymlinkOp{
Target: check.MustAbs("/run/current-system/differs"),
Target: MustAbs("/run/current-system/differs"),
LinkName: "/run/current-system",
Dereference: true,
}, &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, false},
{"linkname differs", &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system/differs",
Dereference: true,
}, &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, false},
{"dereference differs", &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
}, &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, false},
{"equals", &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, true},
@ -118,7 +117,7 @@ func TestSymlinkOp(t *testing.T) {
checkOpMeta(t, []opMetaTestCase{
{"current-system", &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, "creating", `symlink on "/run/current-system" linkname "/run/current-system"`},

View File

@ -7,8 +7,6 @@ import (
"os"
"strconv"
. "syscall"
"hakurei.app/container/check"
)
func init() { gob.Register(new(MountTmpfsOp)) }
@ -20,13 +18,13 @@ func (e TmpfsSizeError) Error() string {
}
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Tmpfs(target *check.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})
return f
}
// Readonly appends an [Op] that mounts read-only tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Readonly(target *check.Absolute, perm os.FileMode) *Ops {
func (f *Ops) Readonly(target *Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, target, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm})
return f
}
@ -34,7 +32,7 @@ func (f *Ops) Readonly(target *check.Absolute, perm os.FileMode) *Ops {
// MountTmpfsOp mounts [FstypeTmpfs] on container Path.
type MountTmpfsOp struct {
FSName string
Path *check.Absolute
Path *Absolute
Flags uintptr
Size int
Perm os.FileMode

View File

@ -5,7 +5,6 @@ import (
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
@ -25,7 +24,7 @@ func TestMountTmpfsOp(t *testing.T) {
{"success", new(Params), &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user/1000/"),
Path: MustAbs("/run/user/1000/"),
Size: 1 << 10,
Perm: 0700,
}, nil, nil, []stub.Call{
@ -43,19 +42,19 @@ func TestMountTmpfsOp(t *testing.T) {
{"nil", (*MountTmpfsOp)(nil), false},
{"zero", new(MountTmpfsOp), false},
{"nil path", &MountTmpfsOp{FSName: "tmpfs"}, false},
{"zero fsname", &MountTmpfsOp{Path: check.MustAbs("/tmp/")}, false},
{"valid", &MountTmpfsOp{FSName: "tmpfs", Path: check.MustAbs("/tmp/")}, true},
{"zero fsname", &MountTmpfsOp{Path: MustAbs("/tmp/")}, false},
{"valid", &MountTmpfsOp{FSName: "tmpfs", Path: MustAbs("/tmp/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"runtime", new(Ops).Tmpfs(
check.MustAbs("/run/user"),
MustAbs("/run/user"),
1<<10,
0755,
), Ops{
&MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@ -63,12 +62,12 @@ func TestMountTmpfsOp(t *testing.T) {
}},
{"nscd", new(Ops).Readonly(
check.MustAbs("/var/run/nscd"),
MustAbs("/var/run/nscd"),
0755,
), Ops{
&MountTmpfsOp{
FSName: "readonly",
Path: check.MustAbs("/var/run/nscd"),
Path: MustAbs("/var/run/nscd"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
Perm: 0755,
},
@ -80,13 +79,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"fsname differs", &MountTmpfsOp{
FSName: "readonly",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@ -94,13 +93,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"path differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user/differs"),
Path: MustAbs("/run/user/differs"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@ -108,13 +107,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"flags differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@ -122,13 +121,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"size differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@ -136,13 +135,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"perm differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0700,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@ -150,13 +149,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"equals", &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@ -166,7 +165,7 @@ func TestMountTmpfsOp(t *testing.T) {
checkOpMeta(t, []opMetaTestCase{
{"runtime", &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"os"
"strings"
. "syscall"
"hakurei.app/container/vfs"
@ -58,6 +59,7 @@ const (
FstypeNULL = zeroString
// FstypeProc represents the proc pseudo-filesystem.
// A fully visible instance of proc must be available in the mount namespace for proc to be mounted.
// This filesystem type is usually mounted on [FHSProc].
FstypeProc = "proc"
// FstypeDevpts represents the devpts pseudo-filesystem.
// This type of filesystem is usually mounted on /dev/pts.
@ -84,20 +86,28 @@ const (
// OptionOverlayUserxattr represents the userxattr option of the overlay pseudo-filesystem.
// Use the "user.overlay." xattr namespace instead of "trusted.overlay.".
OptionOverlayUserxattr = "userxattr"
// SpecialOverlayEscape is the escape string for overlay mount options.
SpecialOverlayEscape = `\`
// SpecialOverlayOption is the separator string between overlay mount options.
SpecialOverlayOption = ","
// SpecialOverlayPath is the separator string between overlay paths.
SpecialOverlayPath = ":"
)
// bindMount mounts source on target and recursively applies flags if MS_REC is set.
func (p *procPaths) bindMount(msg Msg, source, target string, flags uintptr) error {
func (p *procPaths) bindMount(source, target string, flags uintptr) error {
// syscallDispatcher.bindMount and procPaths.remount must not be called from this function
if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
return err
}
return p.k.remount(msg, target, flags)
return p.k.remount(target, flags)
}
// remount applies flags on target, recursively if MS_REC is set.
func (p *procPaths) remount(msg Msg, target string, flags uintptr) error {
func (p *procPaths) remount(target string, flags uintptr) error {
// syscallDispatcher methods bindMount, remount must not be called from this function
var targetFinal string
@ -106,7 +116,7 @@ func (p *procPaths) remount(msg Msg, target string, flags uintptr) error {
} else {
targetFinal = v
if targetFinal != target {
msg.Verbosef("target resolves to %q", targetFinal)
p.k.verbosef("target resolves to %q", targetFinal)
}
}
@ -136,7 +146,7 @@ func (p *procPaths) remount(msg Msg, target string, flags uintptr) error {
return err
}
if err = remountWithFlags(p.k, msg, n, mf); err != nil {
if err = remountWithFlags(p.k, n, mf); err != nil {
return err
}
if flags&MS_REC == 0 {
@ -149,7 +159,7 @@ func (p *procPaths) remount(msg Msg, target string, flags uintptr) error {
continue
}
if err = remountWithFlags(p.k, msg, cur, mf); err != nil && !errors.Is(err, EACCES) {
if err = remountWithFlags(p.k, cur, mf); err != nil && !errors.Is(err, EACCES) {
return err
}
}
@ -159,12 +169,12 @@ func (p *procPaths) remount(msg Msg, target string, flags uintptr) error {
}
// remountWithFlags remounts mount point described by [vfs.MountInfoNode].
func remountWithFlags(k syscallDispatcher, msg Msg, n *vfs.MountInfoNode, mf uintptr) error {
func remountWithFlags(k syscallDispatcher, n *vfs.MountInfoNode, mf uintptr) error {
// syscallDispatcher methods bindMount, remount must not be called from this function
kf, unmatched := n.Flags()
if len(unmatched) != 0 {
msg.Verbosef("unmatched vfs options: %q", unmatched)
k.verbosef("unmatched vfs options: %q", unmatched)
}
if kf&mf != mf {
@ -198,3 +208,20 @@ func parentPerm(perm os.FileMode) os.FileMode {
}
return os.FileMode(pperm)
}
// EscapeOverlayDataSegment escapes a string for formatting into the data argument of an overlay mount call.
func EscapeOverlayDataSegment(s string) string {
if s == zeroString {
return zeroString
}
if f := strings.SplitN(s, "\x00", 2); len(f) > 0 {
s = f[0]
}
return strings.NewReplacer(
SpecialOverlayEscape, SpecialOverlayEscape+SpecialOverlayEscape,
SpecialOverlayOption, SpecialOverlayEscape+SpecialOverlayOption,
SpecialOverlayPath, SpecialOverlayEscape+SpecialOverlayPath,
).Replace(s)
}

View File

@ -11,21 +11,21 @@ import (
func TestBindMount(t *testing.T) {
checkSimple(t, "bindMount", []simpleTestCase{
{"mount", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount(nil, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
{"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)),
}}, stub.UniqueError(0xbad)},
{"success ne", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount(k, "/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY)
{"success ne", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil),
call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil),
}}, nil},
{"success", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount(k, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
{"success", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil),
call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil),
@ -77,29 +77,29 @@ func TestRemount(t *testing.T) {
416 415 0:30 / /sysroot/nix/store ro,relatime master:5 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work`
checkSimple(t, "remount", []simpleTestCase{
{"evalSymlinks", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
{"evalSymlinks", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)),
}}, stub.UniqueError(6)},
{"open", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
{"open", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, stub.UniqueError(5)),
}}, &os.PathError{Op: "open", Path: "/sysroot/nix", Err: stub.UniqueError(5)}},
{"readlink", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
{"readlink", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", stub.UniqueError(4)),
}}, stub.UniqueError(4)},
{"close", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
{"close", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@ -107,8 +107,8 @@ func TestRemount(t *testing.T) {
call("close", stub.ExpectArgs{0xdeadbeef}, nil, stub.UniqueError(3)),
}}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}},
{"mountinfo no match", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(k, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
{"mountinfo no match", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil),
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil),
@ -118,8 +118,8 @@ func TestRemount(t *testing.T) {
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
}}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}},
{"mountinfo", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
{"mountinfo", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@ -128,8 +128,8 @@ func TestRemount(t *testing.T) {
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil),
}}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}},
{"mount", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
{"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@ -139,8 +139,8 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, stub.UniqueError(2)),
}}, stub.UniqueError(2)},
{"mount propagate", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
{"mount propagate", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@ -151,8 +151,8 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)),
}}, stub.UniqueError(1)},
{"success toplevel", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
{"success toplevel", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil),
call("open", stub.ExpectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil),
@ -162,8 +162,8 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil),
}}, nil},
{"success EACCES", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
{"success EACCES", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@ -175,8 +175,8 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil},
{"success no propagate", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
{"success no propagate", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@ -186,8 +186,8 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
}}, nil},
{"success case sensitive", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
{"success case sensitive", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@ -199,8 +199,8 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil},
{"success", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(k, "/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
{"success", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil),
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil),
@ -217,18 +217,18 @@ func TestRemount(t *testing.T) {
func TestRemountWithFlags(t *testing.T) {
checkSimple(t, "remountWithFlags", []simpleTestCase{
{"noop unmatched", func(k *kstub) error {
return remountWithFlags(k, k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
{"noop unmatched", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
}, stub.Expect{Calls: []stub.Call{
call("verbosef", stub.ExpectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil),
}}, nil},
{"noop", func(k *kstub) error {
return remountWithFlags(k, nil, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
{"noop", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
}, stub.Expect{}, nil},
{"success", func(k *kstub) error {
return remountWithFlags(k, nil, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
{"success", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil),
}}, nil},
@ -237,20 +237,20 @@ func TestRemountWithFlags(t *testing.T) {
func TestMountTmpfs(t *testing.T) {
checkSimple(t, "mountTmpfs", []simpleTestCase{
{"mkdirAll", func(k *kstub) error {
{"mkdirAll", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)),
}}, stub.UniqueError(0)},
{"success no size", func(k *kstub) error {
{"success no size", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710)
}, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil),
call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil),
}}, nil},
{"success", func(k *kstub) error {
{"success", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil),
@ -281,3 +281,23 @@ func TestParentPerm(t *testing.T) {
})
}
}
func TestEscapeOverlayDataSegment(t *testing.T) {
testCases := []struct {
name string
s string
want string
}{
{"zero", zeroString, zeroString},
{"multi", `\\\:,:,\\\`, `\\\\\\\:\,\:\,\\\\\\`},
{"bwrap", `/path :,\`, `/path \:\,\\`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := EscapeOverlayDataSegment(tc.s); got != tc.want {
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
}
})
}
}

View File

@ -23,88 +23,45 @@ func GetErrorMessage(err error) (string, bool) {
return e.Message(), true
}
// Msg is used for package-wide verbose logging.
type Msg interface {
// GetLogger returns the address of the underlying [log.Logger].
GetLogger() *log.Logger
// IsVerbose atomically loads and returns whether [Msg] has verbose logging enabled.
IsVerbose() bool
// SwapVerbose atomically stores a new verbose state and returns the previous value held by [Msg].
SwapVerbose(verbose bool) bool
// Verbose passes its argument to the Println method of the underlying [log.Logger] if IsVerbose returns true.
Verbose(v ...any)
// Verbosef passes its argument to the Printf method of the underlying [log.Logger] if IsVerbose returns true.
Verbosef(format string, v ...any)
// Suspend causes the embedded [Suspendable] to withhold writes to its downstream [io.Writer].
// Suspend returns false and is a noop if called between calls to Suspend and Resume.
Suspend() bool
// Resume dumps the entire buffer held by the embedded [Suspendable] and stops withholding future writes.
// Resume returns false and is a noop if a call to Suspend does not precede it.
Suspend()
Resume() bool
// BeforeExit runs implementation-specific cleanup code, and optionally prints warnings.
// BeforeExit is called before [os.Exit].
BeforeExit()
}
// defaultMsg is the default implementation of the [Msg] interface.
// The zero value is not safe for use. Callers should use the [NewMsg] function instead.
type defaultMsg struct {
verbose atomic.Bool
type DefaultMsg struct{ inactive atomic.Bool }
logger *log.Logger
Suspendable
}
// NewMsg initialises a downstream [log.Logger] for a new [Msg].
// The [log.Logger] should no longer be configured after NewMsg returns.
// If downstream is nil, a new logger is initialised in its place.
func NewMsg(downstream *log.Logger) Msg {
if downstream == nil {
downstream = log.New(log.Writer(), "container: ", 0)
}
m := defaultMsg{logger: downstream}
m.Suspendable.Downstream = downstream.Writer()
downstream.SetOutput(&m.Suspendable)
return &m
}
func (msg *defaultMsg) GetLogger() *log.Logger { return msg.logger }
func (msg *defaultMsg) IsVerbose() bool { return msg.verbose.Load() }
func (msg *defaultMsg) SwapVerbose(verbose bool) bool { return msg.verbose.Swap(verbose) }
func (msg *defaultMsg) Verbose(v ...any) {
if msg.verbose.Load() {
msg.logger.Println(v...)
func (msg *DefaultMsg) IsVerbose() bool { return true }
func (msg *DefaultMsg) Verbose(v ...any) {
if !msg.inactive.Load() {
log.Println(v...)
}
}
func (msg *defaultMsg) Verbosef(format string, v ...any) {
if msg.verbose.Load() {
msg.logger.Printf(format, v...)
func (msg *DefaultMsg) Verbosef(format string, v ...any) {
if !msg.inactive.Load() {
log.Printf(format, v...)
}
}
// Resume calls [Suspendable.Resume] and prints a message if buffer was filled
// between calls to [Suspendable.Suspend] and Resume.
func (msg *defaultMsg) Resume() bool {
resumed, dropped, _, err := msg.Suspendable.Resume()
if err != nil {
// probably going to result in an error as well, so this message is as good as unreachable
msg.logger.Printf("cannot dump buffer on resume: %v", err)
}
if resumed && dropped > 0 {
msg.logger.Printf("dropped %d bytes while output is suspended", dropped)
}
return resumed
}
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
func (msg *DefaultMsg) BeforeExit() {}
// BeforeExit prints a message if called between calls to [Suspendable.Suspend] and Resume.
func (msg *defaultMsg) BeforeExit() {
if msg.Resume() {
msg.logger.Printf("beforeExit reached on suspended output")
// msg is the [Msg] implemented used by all exported [container] functions.
var msg Msg = new(DefaultMsg)
// GetOutput returns the current active [Msg] implementation.
func GetOutput() Msg { return msg }
// SetOutput replaces the current active [Msg] implementation.
func SetOutput(v Msg) {
if v == nil {
msg = new(DefaultMsg)
} else {
msg = v
}
}

View File

@ -1,16 +1,14 @@
package container_test
import (
"bytes"
"errors"
"io"
"log"
"strings"
"sync/atomic"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/container/stub"
)
func TestMessageError(t *testing.T) {
@ -41,137 +39,146 @@ func TestMessageError(t *testing.T) {
}
func TestDefaultMsg(t *testing.T) {
// copied from output.go
const suspendBufMax = 1 << 24
t.Run("logger", func(t *testing.T) {
t.Run("nil", func(t *testing.T) {
got := container.NewMsg(nil).GetLogger()
if out := got.Writer().(*container.Suspendable).Downstream; out != log.Writer() {
t.Errorf("GetLogger: Downstream = %#v", out)
{
w := log.Writer()
f := log.Flags()
t.Cleanup(func() { log.SetOutput(w); log.SetFlags(f) })
}
msg := new(container.DefaultMsg)
if prefix := got.Prefix(); prefix != "container: " {
t.Errorf("GetLogger: prefix = %q", prefix)
}
})
t.Run("takeover", func(t *testing.T) {
l := log.New(io.Discard, "\x00", 0xdeadbeef)
got := container.NewMsg(l)
if logger := got.GetLogger(); logger != l {
t.Errorf("GetLogger: %#v, want %#v", logger, l)
}
if ds := l.Writer().(*container.Suspendable).Downstream; ds != io.Discard {
t.Errorf("GetLogger: Downstream = %#v", ds)
}
})
})
dw := expectWriter{t: t}
steps := []struct {
name string
pt, next []byte
err error
f func(t *testing.T, msg container.Msg)
}{
{"zero verbose", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if msg.IsVerbose() {
t.Error("IsVerbose unexpected true")
}
}},
{"swap false", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if msg.SwapVerbose(false) {
t.Error("SwapVerbose unexpected true")
}
}},
{"write discard", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.Verbose("\x00")
msg.Verbosef("\x00")
}},
{"verbose false", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if msg.IsVerbose() {
t.Error("IsVerbose unexpected true")
}
}},
{"swap true", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if msg.SwapVerbose(true) {
t.Error("SwapVerbose unexpected true")
}
}},
{"write verbose", []byte("test: \x00\n"), nil, nil, func(_ *testing.T, msg container.Msg) {
msg.Verbose("\x00")
}},
{"write verbosef", []byte(`test: "\x00"` + "\n"), nil, nil, func(_ *testing.T, msg container.Msg) {
msg.Verbosef("%q", "\x00")
}},
{"verbose true", nil, nil, nil, func(t *testing.T, msg container.Msg) {
t.Run("is verbose", func(t *testing.T) {
if !msg.IsVerbose() {
t.Error("IsVerbose unexpected false")
t.Error("IsVerbose unexpected outcome")
}
})
t.Run("verbose", func(t *testing.T) {
log.SetOutput(panicWriter{})
msg.Suspend()
msg.Verbose()
msg.Verbosef("\x00")
msg.Resume()
buf := new(strings.Builder)
log.SetOutput(buf)
log.SetFlags(0)
msg.Verbose()
msg.Verbosef("\x00")
want := "\n\x00\n"
if buf.String() != want {
t.Errorf("Verbose: %q, want %q", buf.String(), want)
}
})
t.Run("inactive", func(t *testing.T) {
{
inactive := msg.Resume()
if inactive {
t.Cleanup(func() { msg.Suspend() })
}
}
}},
{"resume noop", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if msg.Resume() {
t.Error("Resume unexpected success")
t.Error("Resume unexpected outcome")
}
}},
{"beforeExit noop", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.BeforeExit()
}},
{"beforeExit suspend", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.Suspend()
}},
{"beforeExit message", []byte("test: beforeExit reached on suspended output\n"), nil, nil, func(_ *testing.T, msg container.Msg) {
msg.BeforeExit()
}},
{"post beforeExit resume noop", nil, nil, nil, func(t *testing.T, msg container.Msg) {
if msg.Resume() {
t.Error("Resume unexpected success")
}
}},
{"suspend", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.Suspend()
}},
{"suspend write", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.GetLogger().Print("\x00")
}},
{"resume error", []byte("test: \x00\n"), []byte("test: cannot dump buffer on resume: unique error 0 injected by the test suite\n"), stub.UniqueError(0), func(t *testing.T, msg container.Msg) {
if !msg.Resume() {
t.Error("Resume unexpected failure")
t.Error("Resume unexpected outcome")
}
}},
})
{"suspend drop", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.Suspend()
}},
{"suspend write fill", nil, nil, nil, func(_ *testing.T, msg container.Msg) {
msg.GetLogger().Print(strings.Repeat("\x00", suspendBufMax))
}},
{"resume dropped", append([]byte("test: "), bytes.Repeat([]byte{0}, suspendBufMax-6)...), []byte("test: dropped 7 bytes while output is suspended\n"), nil, func(t *testing.T, msg container.Msg) {
if !msg.Resume() {
t.Error("Resume unexpected failure")
}
}},
}
msg := container.NewMsg(log.New(&dw, "test: ", 0))
for _, step := range steps {
// these share the same writer, so cannot be subtests
t.Logf("running step %q", step.name)
dw.expect, dw.next, dw.err = step.pt, step.next, step.err
step.f(t, msg)
if dw.expect != nil {
t.Errorf("expect: %q", string(dw.expect))
}
}
// the function is a noop
t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() })
}
type panicWriter struct{}
func (panicWriter) Write([]byte) (int, error) { panic("unreachable") }
func saveRestoreOutput(t *testing.T) {
out := container.GetOutput()
t.Cleanup(func() { container.SetOutput(out) })
}
func replaceOutput(t *testing.T) {
saveRestoreOutput(t)
container.SetOutput(&testOutput{t: t})
}
type testOutput struct {
t *testing.T
suspended atomic.Bool
}
func (out *testOutput) IsVerbose() bool { return testing.Verbose() }
func (out *testOutput) Verbose(v ...any) {
if !out.IsVerbose() {
return
}
out.t.Log(v...)
}
func (out *testOutput) Verbosef(format string, v ...any) {
if !out.IsVerbose() {
return
}
out.t.Logf(format, v...)
}
func (out *testOutput) Suspend() {
if out.suspended.CompareAndSwap(false, true) {
out.Verbose("suspend called")
return
}
out.Verbose("suspend called on suspended output")
}
func (out *testOutput) Resume() bool {
if out.suspended.CompareAndSwap(true, false) {
out.Verbose("resume called")
return true
}
out.Verbose("resume called on unsuspended output")
return false
}
func (out *testOutput) BeforeExit() { out.Verbose("beforeExit called") }
func TestGetSetOutput(t *testing.T) {
{
out := container.GetOutput()
t.Cleanup(func() { container.SetOutput(out) })
}
t.Run("default", func(t *testing.T) {
container.SetOutput(new(stubOutput))
if v, ok := container.GetOutput().(*container.DefaultMsg); ok {
t.Fatalf("SetOutput: got unexpected output %#v", v)
}
container.SetOutput(nil)
if _, ok := container.GetOutput().(*container.DefaultMsg); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", container.GetOutput())
}
})
t.Run("stub", func(t *testing.T) {
container.SetOutput(new(stubOutput))
if _, ok := container.GetOutput().(*stubOutput); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", container.GetOutput())
}
})
}
type stubOutput struct {
wrapF func(error, ...any) error
}
func (*stubOutput) IsVerbose() bool { panic("unreachable") }
func (*stubOutput) Verbose(...any) { panic("unreachable") }
func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") }
func (*stubOutput) Suspend() { panic("unreachable") }
func (*stubOutput) Resume() bool { panic("unreachable") }
func (*stubOutput) BeforeExit() { panic("unreachable") }

View File

@ -28,7 +28,7 @@ func TestSuspendable(t *testing.T) {
)
// shares the same writer
steps := []struct {
testCases := []struct {
name string
w, pt []byte
err error
@ -75,25 +75,25 @@ func TestSuspendable(t *testing.T) {
var dw expectWriter
w := container.Suspendable{Downstream: &dw}
for _, step := range steps {
for _, tc := range testCases {
// these share the same writer, so cannot be subtests
t.Logf("writing step %q", step.name)
dw.expect, dw.err = step.pt, step.err
t.Logf("writing step %q", tc.name)
dw.expect, dw.err = tc.pt, tc.err
var (
gotN int
gotErr error
)
wantN := step.n
wantN := tc.n
switch wantN {
case nSpecialPtEquiv:
wantN = len(step.pt)
gotN, gotErr = w.Write(step.w)
wantN = len(tc.pt)
gotN, gotErr = w.Write(tc.w)
case nSpecialWEquiv:
wantN = len(step.w)
gotN, gotErr = w.Write(step.w)
wantN = len(tc.w)
gotN, gotErr = w.Write(tc.w)
case nSpecialSuspend:
s := w.IsSuspended()
@ -101,8 +101,8 @@ func TestSuspendable(t *testing.T) {
t.Fatal("Suspend: unexpected success")
}
wantN = len(step.w)
gotN, gotErr = w.Write(step.w)
wantN = len(tc.w)
gotN, gotErr = w.Write(tc.w)
default:
if wantN <= nSpecialDump {
@ -118,10 +118,10 @@ func TestSuspendable(t *testing.T) {
t.Errorf("Resume: dropped = %d, want %d", dropped, wantDropped)
}
wantN = len(step.pt)
wantN = len(tc.pt)
gotN, gotErr = int(n), err
} else {
gotN, gotErr = w.Write(step.w)
gotN, gotErr = w.Write(tc.w)
}
}
@ -129,13 +129,9 @@ func TestSuspendable(t *testing.T) {
t.Errorf("Write: n = %d, want %d", gotN, wantN)
}
if !reflect.DeepEqual(gotErr, step.wantErr) {
if !reflect.DeepEqual(gotErr, tc.wantErr) {
t.Errorf("Write: %v", gotErr)
}
if dw.expect != nil {
t.Errorf("expect: %q", string(dw.expect))
}
}
}
@ -143,31 +139,17 @@ func TestSuspendable(t *testing.T) {
type expectWriter struct {
expect []byte
err error
// optional consecutive write
next []byte
// optional, calls Error on failure if not nil
t *testing.T
}
func (w *expectWriter) Write(p []byte) (n int, err error) {
defer func() { w.expect = w.next; w.next = nil }()
defer func() { w.expect = nil }()
n, err = len(p), w.err
if w.expect == nil {
n, err = 0, errors.New("unexpected call to Write: "+strconv.Quote(string(p)))
if w.t != nil {
w.t.Error(err.Error())
}
return
return 0, errors.New("unexpected call to Write: " + strconv.Quote(string(p)))
}
if string(p) != string(w.expect) {
n, err = 0, errors.New("p = "+strconv.Quote(string(p))+", want "+strconv.Quote(string(w.expect)))
if w.t != nil {
w.t.Error(err.Error())
}
return
return 0, errors.New("p = " + strconv.Quote(string(p)) + ", want " + strconv.Quote(string(w.expect)))
}
return
}

View File

@ -9,18 +9,84 @@ import (
"strings"
"syscall"
"hakurei.app/container/fhs"
"hakurei.app/container/vfs"
)
/* constants in this file bypass abs check, be extremely careful when changing them! */
const (
// FHSRoot points to the file system root.
FHSRoot = "/"
// FHSEtc points to the directory for system-specific configuration.
FHSEtc = "/etc/"
// FHSTmp points to the place for small temporary files.
FHSTmp = "/tmp/"
// FHSRun points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar.
FHSRun = "/run/"
// FHSRunUser points to a directory containing per-user runtime directories,
// each usually individually mounted "tmpfs" instances.
FHSRunUser = FHSRun + "user/"
// FHSUsr points to vendor-supplied operating system resources.
FHSUsr = "/usr/"
// FHSUsrBin points to binaries and executables for user commands that shall appear in the $PATH search path.
FHSUsrBin = FHSUsr + "bin/"
// FHSVar points to persistent, variable system data. Writable during normal system operation.
FHSVar = "/var/"
// FHSVarLib points to persistent system data.
FHSVarLib = FHSVar + "lib/"
// FHSVarEmpty points to a nonstandard directory that is usually empty.
FHSVarEmpty = FHSVar + "empty/"
// FHSDev points to the root directory for device nodes.
FHSDev = "/dev/"
// FHSProc points to a virtual kernel file system exposing the process list and other functionality.
FHSProc = "/proc/"
// FHSProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
FHSProcSys = FHSProc + "sys/"
// FHSSys points to a virtual kernel file system exposing discovered devices and other functionality.
FHSSys = "/sys/"
)
var (
// AbsFHSRoot is [FHSRoot] as [Absolute].
AbsFHSRoot = &Absolute{FHSRoot}
// AbsFHSEtc is [FHSEtc] as [Absolute].
AbsFHSEtc = &Absolute{FHSEtc}
// AbsFHSTmp is [FHSTmp] as [Absolute].
AbsFHSTmp = &Absolute{FHSTmp}
// AbsFHSRun is [FHSRun] as [Absolute].
AbsFHSRun = &Absolute{FHSRun}
// AbsFHSRunUser is [FHSRunUser] as [Absolute].
AbsFHSRunUser = &Absolute{FHSRunUser}
// AbsFHSUsrBin is [FHSUsrBin] as [Absolute].
AbsFHSUsrBin = &Absolute{FHSUsrBin}
// AbsFHSVar is [FHSVar] as [Absolute].
AbsFHSVar = &Absolute{FHSVar}
// AbsFHSVarLib is [FHSVarLib] as [Absolute].
AbsFHSVarLib = &Absolute{FHSVarLib}
// AbsFHSDev is [FHSDev] as [Absolute].
AbsFHSDev = &Absolute{FHSDev}
// AbsFHSProc is [FHSProc] as [Absolute].
AbsFHSProc = &Absolute{FHSProc}
// AbsFHSSys is [FHSSys] as [Absolute].
AbsFHSSys = &Absolute{FHSSys}
)
const (
// Nonexistent is a path that cannot exist.
// /proc is chosen because a system with covered /proc is unsupported by this package.
Nonexistent = fhs.Proc + "nonexistent"
Nonexistent = FHSProc + "nonexistent"
hostPath = fhs.Root + hostDir
hostPath = FHSRoot + hostDir
hostDir = "host"
sysrootPath = fhs.Root + sysrootDir
sysrootPath = FHSRoot + sysrootDir
sysrootDir = "sysroot"
)

View File

@ -10,7 +10,6 @@ import (
"testing"
"unsafe"
"hakurei.app/container/check"
"hakurei.app/container/vfs"
)
@ -50,8 +49,8 @@ func TestToHost(t *testing.T) {
}
}
// InternalToHostOvlEscape exports toHost passed to [check.EscapeOverlayDataSegment].
func InternalToHostOvlEscape(s string) string { return check.EscapeOverlayDataSegment(toHost(s)) }
// InternalToHostOvlEscape exports toHost passed to EscapeOverlayDataSegment.
func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment(toHost(s)) }
func TestCreateFile(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) {

View File

@ -1,9 +1,6 @@
package seccomp_test
import (
. "hakurei.app/container/bits"
. "hakurei.app/container/seccomp"
)
import . "hakurei.app/container/seccomp"
var bpfExpected = bpfLookup{
{AllowMultiarch | AllowCAN |

View File

@ -1,9 +1,6 @@
package seccomp_test
import (
. "hakurei.app/container/bits"
. "hakurei.app/container/seccomp"
)
import . "hakurei.app/container/seccomp"
var bpfExpected = bpfLookup{
{AllowMultiarch | AllowCAN |

View File

@ -3,14 +3,13 @@ package seccomp_test
import (
"encoding/hex"
"hakurei.app/container/bits"
"hakurei.app/container/seccomp"
)
type (
bpfPreset = struct {
seccomp.ExportFlag
bits.FilterPreset
seccomp.FilterPreset
}
bpfLookup map[bpfPreset][]byte
)

View File

@ -8,7 +8,6 @@ import (
"syscall"
"testing"
. "hakurei.app/container/bits"
. "hakurei.app/container/seccomp"
)

View File

@ -4,33 +4,46 @@ package seccomp
import (
. "syscall"
"hakurei.app/container/bits"
)
func Preset(presets bits.FilterPreset, flags ExportFlag) (rules []NativeRule) {
type FilterPreset int
const (
// PresetExt are project-specific extensions.
PresetExt FilterPreset = 1 << iota
// PresetDenyNS denies namespace setup syscalls.
PresetDenyNS
// PresetDenyTTY denies faking input.
PresetDenyTTY
// PresetDenyDevel denies development-related syscalls.
PresetDenyDevel
// PresetLinux32 sets PER_LINUX32.
PresetLinux32
)
func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) {
allowedPersonality := PER_LINUX
if presets&bits.PresetLinux32 != 0 {
if presets&PresetLinux32 != 0 {
allowedPersonality = PER_LINUX32
}
presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality))
l := len(presetCommon)
if presets&bits.PresetDenyNS != 0 {
if presets&PresetDenyNS != 0 {
l += len(presetNamespace)
}
if presets&bits.PresetDenyTTY != 0 {
if presets&PresetDenyTTY != 0 {
l += len(presetTTY)
}
if presets&bits.PresetDenyDevel != 0 {
if presets&PresetDenyDevel != 0 {
l += len(presetDevelFinal)
}
if flags&AllowMultiarch == 0 {
l += len(presetEmu)
}
if presets&bits.PresetExt != 0 {
if presets&PresetExt != 0 {
l += len(presetCommonExt)
if presets&bits.PresetDenyNS != 0 {
if presets&PresetDenyNS != 0 {
l += len(presetNamespaceExt)
}
if flags&AllowMultiarch == 0 {
@ -40,21 +53,21 @@ func Preset(presets bits.FilterPreset, flags ExportFlag) (rules []NativeRule) {
rules = make([]NativeRule, 0, l)
rules = append(rules, presetCommon...)
if presets&bits.PresetDenyNS != 0 {
if presets&PresetDenyNS != 0 {
rules = append(rules, presetNamespace...)
}
if presets&bits.PresetDenyTTY != 0 {
if presets&PresetDenyTTY != 0 {
rules = append(rules, presetTTY...)
}
if presets&bits.PresetDenyDevel != 0 {
if presets&PresetDenyDevel != 0 {
rules = append(rules, presetDevelFinal...)
}
if flags&AllowMultiarch == 0 {
rules = append(rules, presetEmu...)
}
if presets&bits.PresetExt != 0 {
if presets&PresetExt != 0 {
rules = append(rules, presetCommonExt...)
if presets&bits.PresetDenyNS != 0 {
if presets&PresetDenyNS != 0 {
rules = append(rules, presetNamespaceExt...)
}
if flags&AllowMultiarch == 0 {

View File

@ -8,6 +8,10 @@ import (
"hakurei.app/helper/proc"
)
const (
PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel
)
// New returns an inactive Encoder instance.
func New(rules []NativeRule, flags ExportFlag) *Encoder { return &Encoder{newExporter(rules, flags)} }

View File

@ -2,11 +2,10 @@ package container
import (
"bytes"
"log"
"os"
"strconv"
"sync"
"hakurei.app/container/fhs"
)
var (
@ -18,33 +17,31 @@ var (
)
const (
kernelOverflowuidPath = fhs.ProcSys + "kernel/overflowuid"
kernelOverflowgidPath = fhs.ProcSys + "kernel/overflowgid"
kernelCapLastCapPath = fhs.ProcSys + "kernel/cap_last_cap"
kernelOverflowuidPath = FHSProcSys + "kernel/overflowuid"
kernelOverflowgidPath = FHSProcSys + "kernel/overflowgid"
kernelCapLastCapPath = FHSProcSys + "kernel/cap_last_cap"
)
func mustReadSysctl(msg Msg) {
sysctlOnce.Do(func() {
func mustReadSysctl() {
if v, err := os.ReadFile(kernelOverflowuidPath); err != nil {
msg.GetLogger().Fatalf("cannot read %q: %v", kernelOverflowuidPath, err)
log.Fatalf("cannot read %q: %v", kernelOverflowuidPath, err)
} else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err)
log.Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err)
}
if v, err := os.ReadFile(kernelOverflowgidPath); err != nil {
msg.GetLogger().Fatalf("cannot read %q: %v", kernelOverflowgidPath, err)
log.Fatalf("cannot read %q: %v", kernelOverflowgidPath, err)
} else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err)
log.Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err)
}
if v, err := os.ReadFile(kernelCapLastCapPath); err != nil {
msg.GetLogger().Fatalf("cannot read %q: %v", kernelCapLastCapPath, err)
log.Fatalf("cannot read %q: %v", kernelCapLastCapPath, err)
} else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err)
log.Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err)
}
})
}
func OverflowUid(msg Msg) int { mustReadSysctl(msg); return kernelOverflowuid }
func OverflowGid(msg Msg) int { mustReadSysctl(msg); return kernelOverflowgid }
func LastCap(msg Msg) uintptr { mustReadSysctl(msg); return uintptr(kernelCapLastCap) }
func OverflowUid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowuid }
func OverflowGid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowgid }
func LastCap() uintptr { sysctlOnce.Do(mustReadSysctl); return uintptr(kernelCapLastCap) }

4
dist/comp/_hakurei vendored
View File

@ -54,8 +54,8 @@ __hakurei_instances() {
{
local -a _hakurei_cmds
_hakurei_cmds=(
"app:Load and start container from configuration file"
"run:Configure and start a permissive container"
"app:Load app from configuration file"
"run:Configure and start a permissive default sandbox"
"show:Show live or local app configuration"
"ps:List active instances"
"version:Display version information"

2
dist/install.sh vendored
View File

@ -4,7 +4,7 @@ cd "$(dirname -- "$0")" || exit 1
install -vDm0755 "bin/hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hakurei"
install -vDm0755 "bin/hpkg" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hpkg"
install -vDm4511 "bin/hsu" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hsu"
install -vDm6511 "bin/hsu" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hsu"
if [ ! -f "${HAKUREI_INSTALL_PREFIX}/etc/hsurc" ]; then
install -vDm0400 "hsurc.default" "${HAKUREI_INSTALL_PREFIX}/etc/hsurc"
fi

View File

@ -10,15 +10,13 @@ import (
"sync"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/helper/proc"
)
// New initialises a Helper instance with wt as the null-terminated argument writer.
func New(
ctx context.Context,
msg container.Msg,
pathname *check.Absolute, name string,
pathname *container.Absolute, name string,
wt io.WriterTo,
stat bool,
argF func(argsFd, statFd int) []string,
@ -28,7 +26,7 @@ func New(
var args []string
h := new(helperContainer)
h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
h.Container = container.NewCommand(ctx, msg, pathname, name, args...)
h.Container = container.NewCommand(ctx, pathname, name, args...)
h.WaitDelay = WaitDelay
if cmdF != nil {
cmdF(h.Container)

View File

@ -7,14 +7,12 @@ import (
"testing"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/helper"
)
func TestContainer(t *testing.T) {
t.Run("start invalid container", func(t *testing.T) {
h := helper.New(t.Context(), nil, check.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil)
h := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil)
wantErr := "container: starting an invalid container"
if err := h.Start(); err == nil || err.Error() != wantErr {
@ -24,7 +22,7 @@ func TestContainer(t *testing.T) {
})
t.Run("valid new helper nil check", func(t *testing.T) {
if got := helper.New(t.Context(), nil, check.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil); got == nil {
if got := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil); got == nil {
t.Errorf("New(%q, %q) got nil",
argsWt, "hakurei")
return
@ -33,12 +31,12 @@ func TestContainer(t *testing.T) {
t.Run("implementation compliance", func(t *testing.T) {
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
return helper.New(ctx, nil, check.MustAbs(os.Args[0]), "helper", argsWt, stat, argF, func(z *container.Container) {
return helper.New(ctx, container.MustAbs(os.Args[0]), "helper", argsWt, stat, argF, func(z *container.Container) {
setOutput(&z.Stdout, &z.Stderr)
z.
Bind(fhs.AbsRoot, fhs.AbsRoot, 0).
Proc(fhs.AbsProc).
Dev(fhs.AbsDev, true)
Bind(container.AbsFHSRoot, container.AbsFHSRoot, 0).
Proc(container.AbsFHSProc).
Dev(container.AbsFHSDev, true)
}, nil)
})
})

View File

@ -6,18 +6,11 @@ import (
"os/exec"
"sync/atomic"
"syscall"
"testing"
"time"
)
var FulfillmentTimeout = 2 * time.Second
func init() {
if testing.Testing() {
FulfillmentTimeout *= 10
}
}
// A File is an extra file with deferred initialisation.
type File interface {
// Init initialises File state. Init must not be called more than once.

View File

@ -6,10 +6,12 @@ import (
"hakurei.app/container"
"hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
func TestMain(m *testing.M) {
container.TryArgv0(nil)
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
helper.InternalHelperStub()
os.Exit(m.Run())
}

View File

@ -1,175 +1,109 @@
package hst
import (
"errors"
"strconv"
"time"
"hakurei.app/container/check"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/system/dbus"
)
const Tmp = "/.hakurei"
var AbsTmp = check.MustAbs(Tmp)
const (
// WaitDelayDefault is used when WaitDelay has its zero value.
WaitDelayDefault = 5 * time.Second
// WaitDelayMax is used if WaitDelay exceeds its value.
WaitDelayMax = 30 * time.Second
// IdentityMin is the minimum value of [Config.Identity]. This is enforced by cmd/hsu.
IdentityMin = 0
// IdentityMax is the maximum value of [Config.Identity]. This is enforced by cmd/hsu.
IdentityMax = 9999
// ShimExitRequest is returned when the priv side process requests shim exit.
ShimExitRequest = 254
// ShimExitOrphan is returned when the shim is orphaned before priv side delivers a signal.
ShimExitOrphan = 3
)
var AbsTmp = container.MustAbs(Tmp)
// Config is used to seal an app implementation.
type (
// Config configures an application container, implemented in internal/app.
Config struct {
// Reverse-DNS style configured arbitrary identifier string.
// Passed to wayland security-context-v1 and used as part of defaults in dbus session proxy.
// reverse-DNS style arbitrary identifier string from config;
// passed to wayland security-context-v1 as application ID
// and used as part of defaults in dbus session proxy
ID string `json:"id"`
// System services to make available in the container.
// absolute path to executable file
Path *container.Absolute `json:"path,omitempty"`
// final args passed to container init
Args []string `json:"args"`
// system services to make available in the container
Enablements *Enablements `json:"enablements,omitempty"`
// Session D-Bus proxy configuration.
// If set to nil, session bus proxy assume built-in defaults.
SessionBus *BusConfig `json:"session_bus,omitempty"`
// System D-Bus proxy configuration.
// If set to nil, system bus proxy is disabled.
SystemBus *BusConfig `json:"system_bus,omitempty"`
// Direct access to wayland socket, no attempt is made to attach security-context-v1
// and the bare socket is made available to the container.
// session D-Bus proxy configuration;
// nil makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// system D-Bus proxy configuration;
// nil disables system bus proxy
SystemBus *dbus.Config `json:"system_bus,omitempty"`
// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
// and the bare socket is mounted to the sandbox
DirectWayland bool `json:"direct_wayland,omitempty"`
// Extra acl update ops to perform before setuid.
// passwd username in container, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"`
// absolute path to shell
Shell *container.Absolute `json:"shell"`
// directory to enter and use as home in the container mount namespace
Home *container.Absolute `json:"home"`
// extra acl ops to perform before setuid
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
// Numerical application id, passed to hsu, used to derive init user namespace credentials.
// numerical application id, used for init user namespace credentials
Identity int `json:"identity"`
// Init user namespace supplementary groups inherited by all container processes.
// list of supplementary groups inherited by container processes
Groups []string `json:"groups"`
// High level configuration applied to the underlying [container.Params].
// abstract container configuration baseline
Container *ContainerConfig `json:"container"`
}
// ContainerConfig describes the container configuration to be applied to an underlying [container.Params].
// ContainerConfig describes the container configuration baseline to which the app implementation adds upon.
ContainerConfig struct {
// Container UTS namespace hostname.
// container hostname
Hostname string `json:"hostname,omitempty"`
// Duration in nanoseconds to wait for after interrupting the initial process.
// Defaults to [WaitDelayDefault] if less than or equals to zero,
// or [WaitDelayMax] if greater than [WaitDelayMax].
// duration to wait for after interrupting a container's initial process in nanoseconds;
// a negative value causes the container to be terminated immediately on cancellation
WaitDelay time.Duration `json:"wait_delay,omitempty"`
// Emit Flatpak-compatible seccomp filter programs.
// extra seccomp flags
SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"`
// extra seccomp presets
SeccompPresets seccomp.FilterPreset `json:"seccomp_presets"`
// disable project-specific filter extensions
SeccompCompat bool `json:"seccomp_compat,omitempty"`
// Allow ptrace and friends.
// allow ptrace and friends
Devel bool `json:"devel,omitempty"`
// Allow userns creation and container setup syscalls.
// allow userns creation in container
Userns bool `json:"userns,omitempty"`
// Share host net namespace.
// share host net namespace
HostNet bool `json:"host_net,omitempty"`
// Share abstract unix socket scope.
// share abstract unix socket scope
HostAbstract bool `json:"host_abstract,omitempty"`
// Allow dangerous terminal I/O (faking input).
// allow dangerous terminal I/O
Tty bool `json:"tty,omitempty"`
// Allow multiarch.
// allow multiarch
Multiarch bool `json:"multiarch,omitempty"`
// Initial process environment variables.
// initial process environment variables
Env map[string]string `json:"env"`
/* Map target user uid to privileged user uid in the container 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. */
// 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"`
// Mount /dev/ from the init mount namespace as-is in the container mount namespace.
// pass through all devices
Device bool `json:"device,omitempty"`
/* Container mount points.
If the first element targets /, it is inserted early and excluded from path hiding. */
// container mount points;
// if the first element targets /, it is inserted early and excluded from path hiding
Filesystem []FilesystemConfigJSON `json:"filesystem"`
// String used as the username of the emulated user, validated against the default NAME_REGEX from adduser.
// Defaults to passwd name of target uid or chronos.
Username string `json:"username,omitempty"`
// Pathname of shell in the container filesystem to use for the emulated user.
Shell *check.Absolute `json:"shell"`
// Directory in the container filesystem to enter and use as the home directory of the emulated user.
Home *check.Absolute `json:"home"`
// Pathname to executable file in the container filesystem.
Path *check.Absolute `json:"path,omitempty"`
// Final args passed to the initial program.
Args []string `json:"args"`
}
)
var (
// ErrConfigNull is returned by [Config.Validate] for an invalid configuration that contains a null value for any
// field that must not be null.
ErrConfigNull = errors.New("unexpected null in config")
// ErrIdentityBounds is returned by [Config.Validate] for an out of bounds [Config.Identity] value.
ErrIdentityBounds = errors.New("identity out of bounds")
)
// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
func (config *Config) Validate() error {
if config == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "invalid configuration"}
}
// this is checked again in hsu
if config.Identity < IdentityMin || config.Identity > IdentityMax {
return &AppError{Step: "validate configuration", Err: ErrIdentityBounds,
Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
}
if err := config.SessionBus.CheckInterfaces("session"); err != nil {
return err
}
if err := config.SystemBus.CheckInterfaces("system"); err != nil {
return err
}
if config.Container == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "configuration missing container state"}
}
if config.Container.Home == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "container configuration missing path to home directory"}
}
if config.Container.Shell == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "container configuration missing path to shell"}
}
if config.Container.Path == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "container configuration missing path to initial program"}
}
return nil
}
// ExtraPermConfig describes an acl update op.
type ExtraPermConfig struct {
Ensure bool `json:"ensure,omitempty"`
Path *check.Absolute `json:"path"`
Path *container.Absolute `json:"path"`
Read bool `json:"r,omitempty"`
Write bool `json:"w,omitempty"`
Execute bool `json:"x,omitempty"`

View File

@ -1,57 +1,12 @@
package hst_test
import (
"reflect"
"testing"
"hakurei.app/container/fhs"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestConfigValidate(t *testing.T) {
testCases := []struct {
name string
config *hst.Config
wantErr error
}{
{"nil", nil, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "invalid configuration"}},
{"identity lower", &hst.Config{Identity: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
Msg: "identity -1 out of range"}},
{"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
Msg: "identity 10000 out of range"}},
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}},
&hst.BadInterfaceError{Interface: "", Segment: "session"}},
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}},
&hst.BadInterfaceError{Interface: "", Segment: "system"}},
{"container", &hst.Config{}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "configuration missing container state"}},
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to home directory"}},
{"shell", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to shell"}},
{"path", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to initial program"}},
{"valid", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if err := tc.config.Validate(); !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr)
}
})
}
}
func TestExtraPermConfig(t *testing.T) {
testCases := []struct {
name string
@ -60,14 +15,14 @@ func TestExtraPermConfig(t *testing.T) {
}{
{"nil", nil, "<invalid>"},
{"nil path", &hst.ExtraPermConfig{Path: nil}, "<invalid>"},
{"r", &hst.ExtraPermConfig{Path: fhs.AbsRoot, Read: true}, "r--:/"},
{"r+", &hst.ExtraPermConfig{Ensure: true, Path: fhs.AbsRoot, Read: true}, "r--+:/"},
{"r", &hst.ExtraPermConfig{Path: container.AbsFHSRoot, Read: true}, "r--:/"},
{"r+", &hst.ExtraPermConfig{Ensure: true, Path: container.AbsFHSRoot, Read: true}, "r--+:/"},
{"w", &hst.ExtraPermConfig{Path: hst.AbsTmp, Write: true}, "-w-:/.hakurei"},
{"w+", &hst.ExtraPermConfig{Ensure: true, Path: hst.AbsTmp, Write: true}, "-w-+:/.hakurei"},
{"x", &hst.ExtraPermConfig{Path: fhs.AbsRunUser, Execute: true}, "--x:/run/user/"},
{"x+", &hst.ExtraPermConfig{Ensure: true, Path: fhs.AbsRunUser, Execute: true}, "--x+:/run/user/"},
{"rwx", &hst.ExtraPermConfig{Path: fhs.AbsTmp, Read: true, Write: true, Execute: true}, "rwx:/tmp/"},
{"rwx+", &hst.ExtraPermConfig{Ensure: true, Path: fhs.AbsTmp, Read: true, Write: true, Execute: true}, "rwx+:/tmp/"},
{"x", &hst.ExtraPermConfig{Path: container.AbsFHSRunUser, Execute: true}, "--x:/run/user/"},
{"x+", &hst.ExtraPermConfig{Ensure: true, Path: container.AbsFHSRunUser, Execute: true}, "--x+:/run/user/"},
{"rwx", &hst.ExtraPermConfig{Path: container.AbsFHSTmp, Read: true, Write: true, Execute: true}, "rwx:/tmp/"},
{"rwx+", &hst.ExtraPermConfig{Ensure: true, Path: container.AbsFHSTmp, Read: true, Write: true, Execute: true}, "rwx+:/tmp/"},
}
for _, tc := range testCases {

View File

@ -1,109 +0,0 @@
package hst
import (
"strconv"
"strings"
)
// BadInterfaceError is returned when Interface fails an undocumented check in xdg-dbus-proxy,
// which would have cause a silent failure.
type BadInterfaceError struct {
// Interface is the offending interface string.
Interface string
// Segment is passed through from the [BusConfig.CheckInterfaces] argument.
Segment string
}
func (e *BadInterfaceError) Message() string { return e.Error() }
func (e *BadInterfaceError) Error() string {
if e == nil {
return "<nil>"
}
return "bad interface string " + strconv.Quote(e.Interface) + " in " + e.Segment + " bus configuration"
}
// BusConfig configures the xdg-dbus-proxy process.
type BusConfig struct {
// See set 'see' policy for NAME (--see=NAME)
See []string `json:"see"`
// Talk set 'talk' policy for NAME (--talk=NAME)
Talk []string `json:"talk"`
// Own set 'own' policy for NAME (--own=NAME)
Own []string `json:"own"`
// Call set RULE for calls on NAME (--call=NAME=RULE)
Call map[string]string `json:"call"`
// Broadcast set RULE for broadcasts from NAME (--broadcast=NAME=RULE)
Broadcast map[string]string `json:"broadcast"`
// Log turn on logging (--log)
Log bool `json:"log,omitempty"`
// Filter enable filtering (--filter)
Filter bool `json:"filter"`
}
// Interfaces iterates over all interface strings specified in [BusConfig].
func (c *BusConfig) Interfaces(yield func(string) bool) {
if c == nil {
return
}
for _, iface := range c.See {
if !yield(iface) {
return
}
}
for _, iface := range c.Talk {
if !yield(iface) {
return
}
}
for _, iface := range c.Own {
if !yield(iface) {
return
}
}
for iface := range c.Call {
if !yield(iface) {
return
}
}
for iface := range c.Broadcast {
if !yield(iface) {
return
}
}
}
// CheckInterfaces checks for invalid interface strings based on an undocumented check in xdg-dbus-error,
// returning [BadInterfaceError] if one is encountered.
func (c *BusConfig) CheckInterfaces(segment string) error {
if c == nil {
return nil
}
for iface := range c.Interfaces {
/*
xdg-dbus-proxy fails without output when this condition is not met:
char *dot = strrchr (filter->interface, '.');
if (dot != NULL)
{
*dot = 0;
if (strcmp (dot + 1, "*") != 0)
filter->member = g_strdup (dot + 1);
}
trim ".*" since they are removed before searching for '.':
if (g_str_has_suffix (name, ".*"))
{
name[strlen (name) - 2] = 0;
wildcard = TRUE;
}
*/
if strings.IndexByte(strings.TrimSuffix(iface, ".*"), '.') == -1 {
return &BadInterfaceError{iface, segment}
}
}
return nil
}

View File

@ -1,109 +0,0 @@
package hst_test
import (
"reflect"
"slices"
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestBadInterfaceError(t *testing.T) {
testCases := []struct {
name string
err error
want string
}{
{"nil", (*hst.BadInterfaceError)(nil), "<nil>"},
{"session", &hst.BadInterfaceError{Interface: "\x00", Segment: "session"},
`bad interface string "\x00" in session bus configuration`},
{"system", &hst.BadInterfaceError{Interface: "\x01", Segment: "system"},
`bad interface string "\x01" in system bus configuration`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if gotError := tc.err.Error(); gotError != tc.want {
t.Errorf("Error: %s, want %s", gotError, tc.want)
}
if gotMessage, ok := container.GetErrorMessage(tc.err); !ok {
t.Error("GetErrorMessage: ok = false")
} else if gotMessage != tc.want {
t.Errorf("GetErrorMessage: %s, want %s", gotMessage, tc.want)
}
})
}
}
func TestBusConfigInterfaces(t *testing.T) {
testCases := []struct {
name string
c *hst.BusConfig
cutoff int
want []string
}{
{"nil", nil, 0, nil},
{"all", &hst.BusConfig{
See: []string{"see"}, Talk: []string{"talk"}, Own: []string{"own"},
Call: map[string]string{"call": "unreachable"},
Broadcast: map[string]string{"broadcast": "unreachable"},
}, 0, []string{"see", "talk", "own", "call", "broadcast"}},
{"all cutoff", &hst.BusConfig{
See: []string{"see"}, Talk: []string{"talk"}, Own: []string{"own"},
Call: map[string]string{"call": "unreachable"},
Broadcast: map[string]string{"broadcast": "unreachable"},
}, 3, []string{"see", "talk", "own"}},
{"cutoff see", &hst.BusConfig{See: []string{"see"}}, 1, []string{"see"}},
{"cutoff talk", &hst.BusConfig{Talk: []string{"talk"}}, 1, []string{"talk"}},
{"cutoff own", &hst.BusConfig{Own: []string{"own"}}, 1, []string{"own"}},
{"cutoff call", &hst.BusConfig{Call: map[string]string{"call": "unreachable"}}, 1, []string{"call"}},
{"cutoff broadcast", &hst.BusConfig{Broadcast: map[string]string{"broadcast": "unreachable"}}, 1, []string{"broadcast"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var got []string
if tc.cutoff > 0 {
var i int
got = make([]string, 0, tc.cutoff)
for v := range tc.c.Interfaces {
i++
got = append(got, v)
if i == tc.cutoff {
break
}
}
} else {
got = slices.Collect(tc.c.Interfaces)
}
if !slices.Equal(got, tc.want) {
t.Errorf("Interfaces: %q, want %q", got, tc.want)
}
})
}
}
func TestBusConfigCheckInterfaces(t *testing.T) {
testCases := []struct {
name string
c *hst.BusConfig
err error
}{
{"nil", nil, nil},
{"zero", &hst.BusConfig{See: []string{""}},
&hst.BadInterfaceError{Interface: "", Segment: "zero"}},
{"suffix", &hst.BusConfig{See: []string{".*"}},
&hst.BadInterfaceError{Interface: ".*", Segment: "suffix"}},
{"valid suffix", &hst.BusConfig{See: []string{"..*"}}, nil},
{"valid", &hst.BusConfig{See: []string{"."}}, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if err := tc.c.CheckInterfaces(tc.name); !reflect.DeepEqual(err, tc.err) {
t.Errorf("CheckInterfaces: error = %#v, want %#v", err, tc.err)
}
})
}
}

View File

@ -2,56 +2,15 @@ package hst
import (
"encoding/json"
"fmt"
"strings"
"syscall"
"hakurei.app/system"
)
// Enablement represents an optional host service to export to the target user.
type Enablement byte
// NewEnablements returns the address of [system.Enablement] as [Enablements].
func NewEnablements(e system.Enablement) *Enablements { return (*Enablements)(&e) }
const (
EWayland Enablement = 1 << iota
EX11
EDBus
EPulse
EM
)
func (e Enablement) String() string {
switch e {
case 0:
return "(no enablements)"
case EWayland:
return "wayland"
case EX11:
return "x11"
case EDBus:
return "dbus"
case EPulse:
return "pulseaudio"
default:
buf := new(strings.Builder)
buf.Grow(32)
for i := Enablement(1); i < EM; i <<= 1 {
if e&i != 0 {
buf.WriteString(", " + i.String())
}
}
if buf.Len() == 0 {
return fmt.Sprintf("e%x", byte(e))
}
return strings.TrimPrefix(buf.String(), ", ")
}
}
// NewEnablements returns the address of [Enablement] as [Enablements].
func NewEnablements(e Enablement) *Enablements { return (*Enablements)(&e) }
// enablementsJSON is the [json] representation of the [Enablement] bit field.
// enablementsJSON is the [json] representation of the [system.Enablement] bit field.
type enablementsJSON struct {
Wayland bool `json:"wayland,omitempty"`
X11 bool `json:"x11,omitempty"`
@ -59,15 +18,15 @@ type enablementsJSON struct {
Pulse bool `json:"pulse,omitempty"`
}
// Enablements is the [json] adapter for [Enablement].
type Enablements Enablement
// Enablements is the [json] adapter for [system.Enablement].
type Enablements system.Enablement
// Unwrap returns the underlying [Enablement].
func (e *Enablements) Unwrap() Enablement {
// Unwrap returns the underlying [system.Enablement].
func (e *Enablements) Unwrap() system.Enablement {
if e == nil {
return 0
}
return Enablement(*e)
return system.Enablement(*e)
}
func (e *Enablements) MarshalJSON() ([]byte, error) {
@ -75,10 +34,10 @@ func (e *Enablements) MarshalJSON() ([]byte, error) {
return nil, syscall.EINVAL
}
return json.Marshal(&enablementsJSON{
Wayland: Enablement(*e)&EWayland != 0,
X11: Enablement(*e)&EX11 != 0,
DBus: Enablement(*e)&EDBus != 0,
Pulse: Enablement(*e)&EPulse != 0,
Wayland: system.Enablement(*e)&system.EWayland != 0,
X11: system.Enablement(*e)&system.EX11 != 0,
DBus: system.Enablement(*e)&system.EDBus != 0,
Pulse: system.Enablement(*e)&system.EPulse != 0,
})
}
@ -92,18 +51,18 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
return err
}
var ve Enablement
var ve system.Enablement
if v.Wayland {
ve |= EWayland
ve |= system.EWayland
}
if v.X11 {
ve |= EX11
ve |= system.EX11
}
if v.DBus {
ve |= EDBus
ve |= system.EDBus
}
if v.Pulse {
ve |= EPulse
ve |= system.EPulse
}
*e = Enablements(ve)
return nil

View File

@ -7,44 +7,9 @@ import (
"testing"
"hakurei.app/hst"
"hakurei.app/system"
)
func TestEnablementString(t *testing.T) {
testCases := []struct {
flags hst.Enablement
want string
}{
{0, "(no enablements)"},
{hst.EWayland, "wayland"},
{hst.EX11, "x11"},
{hst.EDBus, "dbus"},
{hst.EPulse, "pulseaudio"},
{hst.EWayland | hst.EX11, "wayland, x11"},
{hst.EWayland | hst.EDBus, "wayland, dbus"},
{hst.EWayland | hst.EPulse, "wayland, pulseaudio"},
{hst.EX11 | hst.EDBus, "x11, dbus"},
{hst.EX11 | hst.EPulse, "x11, pulseaudio"},
{hst.EDBus | hst.EPulse, "dbus, pulseaudio"},
{hst.EWayland | hst.EX11 | hst.EDBus, "wayland, x11, dbus"},
{hst.EWayland | hst.EX11 | hst.EPulse, "wayland, x11, pulseaudio"},
{hst.EWayland | hst.EDBus | hst.EPulse, "wayland, dbus, pulseaudio"},
{hst.EX11 | hst.EDBus | hst.EPulse, "x11, dbus, pulseaudio"},
{hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse, "wayland, x11, dbus, pulseaudio"},
{1 << 5, "e20"},
{1 << 6, "e40"},
{1 << 7, "e80"},
}
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
if got := tc.flags.String(); got != tc.want {
t.Errorf("String: %q, want %q", got, tc.want)
}
})
}
}
func TestEnablements(t *testing.T) {
testCases := []struct {
name string
@ -54,11 +19,11 @@ func TestEnablements(t *testing.T) {
}{
{"nil", nil, "null", `{"value":null,"magic":3236757504}`},
{"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`},
{"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
{"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
{"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
{"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
{"all", hst.NewEnablements(hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse), `{"wayland":true,"x11":true,"dbus":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pulse":true},"magic":3236757504}`},
{"wayland", hst.NewEnablements(system.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
{"x11", hst.NewEnablements(system.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
{"dbus", hst.NewEnablements(system.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
{"pulse", hst.NewEnablements(system.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
{"all", hst.NewEnablements(system.EWayland | system.EX11 | system.EDBus | system.EPulse), `{"wayland":true,"x11":true,"dbus":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pulse":true},"magic":3236757504}`},
}
for _, tc := range testCases {
@ -123,7 +88,7 @@ func TestEnablements(t *testing.T) {
})
t.Run("val", func(t *testing.T) {
if got := hst.NewEnablements(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
if got := hst.NewEnablements(system.EWayland | system.EPulse).Unwrap(); got != system.EWayland|system.EPulse {
t.Errorf("Unwrap: %v", got)
}
})

View File

@ -4,10 +4,9 @@ import (
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"hakurei.app/container/check"
"hakurei.app/container"
)
// FilesystemConfig is an abstract representation of a mount point.
@ -15,44 +14,21 @@ type FilesystemConfig interface {
// Valid returns whether the configuration is valid.
Valid() bool
// Path returns the target path in the container.
Path() *check.Absolute
Path() *container.Absolute
// Host returns a slice of all host paths used by this operation.
Host() []*check.Absolute
Host() []*container.Absolute
// Apply appends the [container.Op] implementing this operation.
Apply(z *ApplyState)
fmt.Stringer
}
// The Ops interface enables [FilesystemConfig] to queue container ops without depending on the container package.
type Ops interface {
// Tmpfs appends an op that mounts tmpfs on a container path.
Tmpfs(target *check.Absolute, size int, perm os.FileMode) Ops
// Readonly appends an op that mounts read-only tmpfs on a container path.
Readonly(target *check.Absolute, perm os.FileMode) Ops
// Bind appends an op that bind mounts a host path on a container path.
Bind(source, target *check.Absolute, flags int) Ops
// Overlay appends an op that mounts the overlay pseudo filesystem.
Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) Ops
// OverlayReadonly appends an op that mounts the overlay pseudo filesystem readonly.
OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) Ops
// Link appends an op that creates a symlink in the container filesystem.
Link(target *check.Absolute, linkName string, dereference bool) Ops
// Root appends an op that expands a directory into a toplevel bind mount mirror on container root.
Root(host *check.Absolute, flags int) Ops
// Etc appends an op that expands host /etc into a toplevel symlink mirror with /etc semantics.
Etc(host *check.Absolute, prefix string) Ops
}
// ApplyState holds the address of [container.Ops] and any relevant application state.
type ApplyState struct {
// AutoEtcPrefix is the prefix for [container.AutoEtcOp].
AutoEtcPrefix string
Ops
*container.Ops
}
var (

View File

@ -3,14 +3,12 @@ package hst_test
import (
"encoding/json"
"errors"
"os"
"reflect"
"strings"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/hst"
)
@ -219,8 +217,8 @@ type stubFS struct {
}
func (s stubFS) Valid() bool { return false }
func (s stubFS) Path() *check.Absolute { panic("unreachable") }
func (s stubFS) Host() []*check.Absolute { panic("unreachable") }
func (s stubFS) Path() *container.Absolute { panic("unreachable") }
func (s stubFS) Host() []*container.Absolute { panic("unreachable") }
func (s stubFS) Apply(*hst.ApplyState) { panic("unreachable") }
func (s stubFS) String() string { return "<invalid " + s.typeName + ">" }
@ -234,8 +232,8 @@ type fsTestCase struct {
fs hst.FilesystemConfig
valid bool
ops container.Ops
path *check.Absolute
host []*check.Absolute
path *container.Absolute
host []*container.Absolute
str string
}
@ -250,7 +248,7 @@ func checkFs(t *testing.T, testCases []fsTestCase) {
t.Run("ops", func(t *testing.T) {
ops := new(container.Ops)
tc.fs.Apply(&hst.ApplyState{AutoEtcPrefix: ":3", Ops: opsAdapter{ops}})
tc.fs.Apply(&hst.ApplyState{AutoEtcPrefix: ":3", Ops: ops})
if !reflect.DeepEqual(ops, &tc.ops) {
gotString := new(strings.Builder)
for _, op := range *ops {
@ -289,45 +287,11 @@ func checkFs(t *testing.T, testCases []fsTestCase) {
}
}
type opsAdapter struct{ *container.Ops }
func (p opsAdapter) Tmpfs(target *check.Absolute, size int, perm os.FileMode) hst.Ops {
return opsAdapter{p.Ops.Tmpfs(target, size, perm)}
}
func (p opsAdapter) Readonly(target *check.Absolute, perm os.FileMode) hst.Ops {
return opsAdapter{p.Ops.Readonly(target, perm)}
}
func (p opsAdapter) Bind(source, target *check.Absolute, flags int) hst.Ops {
return opsAdapter{p.Ops.Bind(source, target, flags)}
}
func (p opsAdapter) Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) hst.Ops {
return opsAdapter{p.Ops.Overlay(target, state, work, layers...)}
}
func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
return opsAdapter{p.Ops.OverlayReadonly(target, layers...)}
}
func (p opsAdapter) Link(target *check.Absolute, linkName string, dereference bool) hst.Ops {
return opsAdapter{p.Ops.Link(target, linkName, dereference)}
}
func (p opsAdapter) Root(host *check.Absolute, flags int) hst.Ops {
return opsAdapter{p.Ops.Root(host, flags)}
}
func (p opsAdapter) Etc(host *check.Absolute, prefix string) hst.Ops {
return opsAdapter{p.Ops.Etc(host, prefix)}
}
func m(pathname string) *check.Absolute { return check.MustAbs(pathname) }
func ms(pathnames ...string) []*check.Absolute {
as := make([]*check.Absolute, len(pathnames))
func m(pathname string) *container.Absolute { return container.MustAbs(pathname) }
func ms(pathnames ...string) []*container.Absolute {
as := make([]*container.Absolute, len(pathnames))
for i, pathname := range pathnames {
as[i] = check.MustAbs(pathname)
as[i] = container.MustAbs(pathname)
}
return as
}

View File

@ -4,9 +4,7 @@ import (
"encoding/gob"
"strings"
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container"
)
func init() { gob.Register(new(FSBind)) }
@ -17,9 +15,9 @@ const FilesystemBind = "bind"
// FSBind represents a host to container bind mount.
type FSBind struct {
// mount point in container, same as Source if empty
Target *check.Absolute `json:"dst,omitempty"`
Target *container.Absolute `json:"dst,omitempty"`
// host filesystem path to make available to the container
Source *check.Absolute `json:"src"`
Source *container.Absolute `json:"src"`
// do not mount Target read-only
Write bool `json:"write,omitempty"`
// do not disable device files on Target, implies Write
@ -30,19 +28,19 @@ type FSBind struct {
Optional bool `json:"optional,omitempty"`
// enable special behaviour:
// for autoroot, Target must be set to [fhs.AbsRoot];
// for autoetc, Target must be set to [fhs.AbsEtc]
// for autoroot, Target must be set to [container.AbsFHSRoot];
// for autoetc, Target must be set to [container.AbsFHSEtc]
Special bool `json:"special,omitempty"`
}
// IsAutoRoot returns whether this FSBind has autoroot behaviour enabled.
func (b *FSBind) IsAutoRoot() bool {
return b.Valid() && b.Special && b.Target.String() == fhs.Root
return b.Valid() && b.Special && b.Target.String() == container.FHSRoot
}
// IsAutoEtc returns whether this FSBind has autoetc behaviour enabled.
func (b *FSBind) IsAutoEtc() bool {
return b.Valid() && b.Special && b.Target.String() == fhs.Etc
return b.Valid() && b.Special && b.Target.String() == container.FHSEtc
}
func (b *FSBind) Valid() bool {
@ -57,7 +55,7 @@ func (b *FSBind) Valid() bool {
return false
} else {
switch b.Target.String() {
case fhs.Root, fhs.Etc:
case container.FHSRoot, container.FHSEtc:
break
default:
@ -68,7 +66,7 @@ func (b *FSBind) Valid() bool {
return true
}
func (b *FSBind) Path() *check.Absolute {
func (b *FSBind) Path() *container.Absolute {
if !b.Valid() {
return nil
}
@ -78,11 +76,11 @@ func (b *FSBind) Path() *check.Absolute {
return b.Target
}
func (b *FSBind) Host() []*check.Absolute {
func (b *FSBind) Host() []*container.Absolute {
if !b.Valid() {
return nil
}
return []*check.Absolute{b.Source}
return []*container.Absolute{b.Source}
}
func (b *FSBind) Apply(z *ApplyState) {
@ -96,16 +94,16 @@ func (b *FSBind) Apply(z *ApplyState) {
}
var flags int
if b.Write {
flags |= bits.BindWritable
flags |= container.BindWritable
}
if b.Device {
flags |= bits.BindDevice | bits.BindWritable
flags |= container.BindDevice | container.BindWritable
}
if b.Ensure {
flags |= bits.BindEnsure
flags |= container.BindEnsure
}
if b.Optional {
flags |= bits.BindOptional
flags |= container.BindOptional
}
switch {
@ -139,7 +137,7 @@ func (b *FSBind) String() string {
if flagSym != "" {
prefix += ":" + flagSym
}
if b.Source.String() != fhs.Root {
if b.Source.String() != container.FHSRoot {
return prefix + ":" + b.Source.String()
}
return prefix

View File

@ -4,7 +4,6 @@ import (
"testing"
"hakurei.app/container"
"hakurei.app/container/bits"
"hakurei.app/hst"
)
@ -22,7 +21,7 @@ func TestFSBind(t *testing.T) {
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/dev"),
Target: m("/dev"),
Flags: bits.BindWritable | bits.BindDevice | bits.BindOptional,
Flags: container.BindWritable | container.BindDevice | container.BindOptional,
}}, m("/dev"), ms("/mnt/dev"),
"d+/mnt/dev:/dev"},
@ -34,7 +33,7 @@ func TestFSBind(t *testing.T) {
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/dev"),
Target: m("/dev"),
Flags: bits.BindWritable | bits.BindDevice | bits.BindEnsure,
Flags: container.BindWritable | container.BindDevice | container.BindEnsure,
}}, m("/dev"), ms("/mnt/dev"),
"d-/mnt/dev:/dev"},
@ -46,7 +45,7 @@ func TestFSBind(t *testing.T) {
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/dev"),
Target: m("/dev"),
Flags: bits.BindWritable | bits.BindDevice,
Flags: container.BindWritable | container.BindDevice,
}}, m("/dev"), ms("/mnt/dev"),
"d*/mnt/dev:/dev"},
@ -57,7 +56,7 @@ func TestFSBind(t *testing.T) {
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/tmp"),
Target: m("/tmp"),
Flags: bits.BindWritable,
Flags: container.BindWritable,
}}, m("/tmp"), ms("/mnt/tmp"),
"w*/mnt/tmp:/tmp"},
@ -96,7 +95,7 @@ func TestFSBind(t *testing.T) {
Special: true,
}, true, container.Ops{&container.AutoRootOp{
Host: m("/"),
Flags: bits.BindWritable,
Flags: container.BindWritable,
}}, m("/"), ms("/"), "autoroot:w"},
{"autoroot silly", &hst.FSBind{
@ -106,7 +105,7 @@ func TestFSBind(t *testing.T) {
Special: true,
}, true, container.Ops{&container.AutoRootOp{
Host: m("/etc"),
Flags: bits.BindWritable,
Flags: container.BindWritable,
}}, m("/"), ms("/etc"), "autoroot:w:/etc"},
{"autoetc", &hst.FSBind{

View File

@ -5,7 +5,7 @@ import (
"os"
"strings"
"hakurei.app/container/check"
"hakurei.app/container"
)
func init() { gob.Register(new(FSEphemeral)) }
@ -16,7 +16,7 @@ const FilesystemEphemeral = "ephemeral"
// FSEphemeral represents an ephemeral container mount point.
type FSEphemeral struct {
// mount point in container
Target *check.Absolute `json:"dst,omitempty"`
Target *container.Absolute `json:"dst,omitempty"`
// do not mount filesystem read-only
Write bool `json:"write,omitempty"`
// upper limit on the size of the filesystem
@ -27,14 +27,14 @@ type FSEphemeral struct {
func (e *FSEphemeral) Valid() bool { return e != nil && e.Target != nil }
func (e *FSEphemeral) Path() *check.Absolute {
func (e *FSEphemeral) Path() *container.Absolute {
if !e.Valid() {
return nil
}
return e.Target
}
func (e *FSEphemeral) Host() []*check.Absolute { return nil }
func (e *FSEphemeral) Host() []*container.Absolute { return nil }
const fsEphemeralDefaultPerm = os.FileMode(0755)

View File

@ -4,7 +4,7 @@ import (
"encoding/gob"
"path"
"hakurei.app/container/check"
"hakurei.app/container"
)
func init() { gob.Register(new(FSLink)) }
@ -15,7 +15,7 @@ const FilesystemLink = "link"
// FSLink represents a symlink in the container filesystem.
type FSLink struct {
// link path in container
Target *check.Absolute `json:"dst"`
Target *container.Absolute `json:"dst"`
// linkname the symlink points to
Linkname string `json:"linkname"`
// whether to dereference linkname before creating the link
@ -29,14 +29,14 @@ func (l *FSLink) Valid() bool {
return !l.Dereference || path.IsAbs(l.Linkname)
}
func (l *FSLink) Path() *check.Absolute {
func (l *FSLink) Path() *container.Absolute {
if !l.Valid() {
return nil
}
return l.Target
}
func (l *FSLink) Host() []*check.Absolute { return nil }
func (l *FSLink) Host() []*container.Absolute { return nil }
func (l *FSLink) Apply(z *ApplyState) {
if !l.Valid() {

View File

@ -4,7 +4,7 @@ import (
"encoding/gob"
"strings"
"hakurei.app/container/check"
"hakurei.app/container"
)
func init() { gob.Register(new(FSOverlay)) }
@ -15,14 +15,14 @@ const FilesystemOverlay = "overlay"
// FSOverlay represents an overlay mount point.
type FSOverlay struct {
// mount point in container
Target *check.Absolute `json:"dst"`
Target *container.Absolute `json:"dst"`
// any filesystem, does not need to be on a writable filesystem, must not be nil
Lower []*check.Absolute `json:"lower"`
Lower []*container.Absolute `json:"lower"`
// the upperdir is normally on a writable filesystem, leave as nil to mount Lower readonly
Upper *check.Absolute `json:"upper,omitempty"`
Upper *container.Absolute `json:"upper,omitempty"`
// the workdir needs to be an empty directory on the same filesystem as Upper, must not be nil if Upper is populated
Work *check.Absolute `json:"work,omitempty"`
Work *container.Absolute `json:"work,omitempty"`
}
func (o *FSOverlay) Valid() bool {
@ -43,18 +43,18 @@ func (o *FSOverlay) Valid() bool {
}
}
func (o *FSOverlay) Path() *check.Absolute {
func (o *FSOverlay) Path() *container.Absolute {
if !o.Valid() {
return nil
}
return o.Target
}
func (o *FSOverlay) Host() []*check.Absolute {
func (o *FSOverlay) Host() []*container.Absolute {
if !o.Valid() {
return nil
}
p := make([]*check.Absolute, 0, 2+len(o.Lower))
p := make([]*container.Absolute, 0, 2+len(o.Lower))
if o.Upper != nil && o.Work != nil {
p = append(p, o.Upper, o.Work)
}
@ -81,18 +81,18 @@ func (o *FSOverlay) String() string {
lower := make([]string, len(o.Lower))
for i, a := range o.Lower {
lower[i] = check.EscapeOverlayDataSegment(a.String())
lower[i] = container.EscapeOverlayDataSegment(a.String())
}
if o.Upper != nil && o.Work != nil {
return "w*" + strings.Join(append([]string{
check.EscapeOverlayDataSegment(o.Target.String()),
check.EscapeOverlayDataSegment(o.Upper.String()),
check.EscapeOverlayDataSegment(o.Work.String())},
lower...), check.SpecialOverlayPath)
container.EscapeOverlayDataSegment(o.Target.String()),
container.EscapeOverlayDataSegment(o.Upper.String()),
container.EscapeOverlayDataSegment(o.Work.String())},
lower...), container.SpecialOverlayPath)
} else {
return "*" + strings.Join(append([]string{
check.EscapeOverlayDataSegment(o.Target.String())},
lower...), check.SpecialOverlayPath)
container.EscapeOverlayDataSegment(o.Target.String())},
lower...), container.SpecialOverlayPath)
}
}

View File

@ -4,14 +4,13 @@ import (
"testing"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/hst"
)
func TestFSOverlay(t *testing.T) {
checkFs(t, []fsTestCase{
{"nil", (*hst.FSOverlay)(nil), false, nil, nil, nil, "<invalid>"},
{"nil lower", &hst.FSOverlay{Target: m("/etc"), Lower: []*check.Absolute{nil}}, false, nil, nil, nil, "<invalid>"},
{"nil lower", &hst.FSOverlay{Target: m("/etc"), Lower: []*container.Absolute{nil}}, false, nil, nil, nil, "<invalid>"},
{"zero lower", &hst.FSOverlay{Target: m("/etc"), Upper: m("/"), Work: m("/")}, false, nil, nil, nil, "<invalid>"},
{"zero lower ro", &hst.FSOverlay{Target: m("/etc")}, false, nil, nil, nil, "<invalid>"},
{"short lower", &hst.FSOverlay{Target: m("/etc"), Lower: ms("/etc")}, false, nil, nil, nil, "<invalid>"},

View File

@ -6,8 +6,10 @@ import (
"net"
"os"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
// An AppError is returned while starting an app according to [hst.Config].
@ -39,13 +41,13 @@ func (e *AppError) Message() string {
// Paths contains environment-dependent paths used by hakurei.
type Paths struct {
// temporary directory returned by [os.TempDir] (usually `/tmp`)
TempDir *check.Absolute `json:"temp_dir"`
TempDir *container.Absolute `json:"temp_dir"`
// path to shared directory (usually `/tmp/hakurei.%d`, [Info.User])
SharePath *check.Absolute `json:"share_path"`
SharePath *container.Absolute `json:"share_path"`
// XDG_RUNTIME_DIR value (usually `/run/user/%d`, uid)
RuntimePath *check.Absolute `json:"runtime_path"`
RuntimePath *container.Absolute `json:"runtime_path"`
// application runtime directory (usually `/run/user/%d/hakurei`)
RunDirPath *check.Absolute `json:"run_dir_path"`
RunDirPath *container.Absolute `json:"run_dir_path"`
}
type Info struct {
@ -60,9 +62,18 @@ func Template() *Config {
return &Config{
ID: "org.chromium.Chromium",
Enablements: NewEnablements(EWayland | EDBus | EPulse),
Path: container.AbsFHSRun.Append("current-system/sw/bin/chromium"),
Args: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland",
},
SessionBus: &BusConfig{
Enablements: NewEnablements(system.EWayland | system.EDBus | system.EPulse),
SessionBus: &dbus.Config{
See: nil,
Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver",
"org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"},
@ -73,7 +84,7 @@ func Template() *Config {
Log: false,
Filter: true,
},
SystemBus: &BusConfig{
SystemBus: &dbus.Config{
See: nil,
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Own: nil,
@ -84,9 +95,12 @@ func Template() *Config {
},
DirectWayland: false,
Username: "chronos",
Shell: container.AbsFHSRun.Append("current-system/sw/bin/zsh"),
Home: container.MustAbs("/data/data/org.chromium.Chromium"),
ExtraPerms: []*ExtraPermConfig{
{Path: fhs.AbsVarLib.Append("hakurei/u0"), Ensure: true, Execute: true},
{Path: fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"), Read: true, Write: true, Execute: true},
{Path: container.AbsFHSVarLib.Append("hakurei/u0"), Ensure: true, Execute: true},
{Path: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"), Read: true, Write: true, Execute: true},
},
Identity: 9,
@ -100,6 +114,8 @@ func Template() *Config {
HostAbstract: true,
Device: true,
WaitDelay: -1,
SeccompFlags: seccomp.AllowMultiarch,
SeccompPresets: seccomp.PresetExt,
SeccompCompat: true,
Tty: true,
Multiarch: true,
@ -112,34 +128,21 @@ func Template() *Config {
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
},
Filesystem: []FilesystemConfigJSON{
{&FSBind{Target: fhs.AbsRoot, Source: fhs.AbsVarLib.Append("hakurei/base/org.debian"), Write: true, Special: true}},
{&FSBind{Target: fhs.AbsEtc, Source: fhs.AbsEtc, Special: true}},
{&FSEphemeral{Target: fhs.AbsTmp, Write: true, Perm: 0755}},
{&FSBind{Target: container.AbsFHSRoot, Source: container.AbsFHSVarLib.Append("hakurei/base/org.debian"), Write: true, Special: true}},
{&FSBind{Target: container.AbsFHSEtc, Source: container.AbsFHSEtc, Special: true}},
{&FSEphemeral{Target: container.AbsFHSTmp, Write: true, Perm: 0755}},
{&FSOverlay{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: container.MustAbs("/nix/store"),
Lower: []*container.Absolute{container.MustAbs("/mnt-root/nix/.ro-store")},
Upper: container.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: container.MustAbs("/mnt-root/nix/.rw-store/work"),
}},
{&FSBind{Source: check.MustAbs("/nix/store")}},
{&FSLink{Target: fhs.AbsRun.Append("current-system"), Linkname: "/run/current-system", Dereference: true}},
{&FSLink{Target: fhs.AbsRun.Append("opengl-driver"), Linkname: "/run/opengl-driver", Dereference: true}},
{&FSBind{Source: fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"),
Target: check.MustAbs("/data/data/org.chromium.Chromium"), Write: true, Ensure: true}},
{&FSBind{Source: fhs.AbsDev.Append("dri"), Device: true, Optional: true}},
},
Username: "chronos",
Shell: fhs.AbsRun.Append("current-system/sw/bin/zsh"),
Home: check.MustAbs("/data/data/org.chromium.Chromium"),
Path: fhs.AbsRun.Append("current-system/sw/bin/chromium"),
Args: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland",
{&FSBind{Source: container.MustAbs("/nix/store")}},
{&FSLink{Target: container.AbsFHSRun.Append("current-system"), Linkname: "/run/current-system", Dereference: true}},
{&FSLink{Target: container.AbsFHSRun.Append("opengl-driver"), Linkname: "/run/opengl-driver", Dereference: true}},
{&FSBind{Source: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"),
Target: container.MustAbs("/data/data/org.chromium.Chromium"), Write: true, Ensure: true}},
{&FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}},
},
},
}

View File

@ -92,6 +92,14 @@ func TestAppError(t *testing.T) {
func TestTemplate(t *testing.T) {
const want = `{
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": {
"wayland": true,
"dbus": true,
@ -133,6 +141,9 @@ func TestTemplate(t *testing.T) {
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
@ -155,6 +166,8 @@ func TestTemplate(t *testing.T) {
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
@ -227,17 +240,6 @@ func TestTemplate(t *testing.T) {
"dev": true,
"optional": true
}
],
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
]
}
}`

View File

@ -6,24 +6,23 @@ import (
"log"
"os"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
)
// Main runs an app according to [hst.Config] and terminates. Main does not return.
func Main(ctx context.Context, msg container.Msg, config *hst.Config) {
func Main(ctx context.Context, config *hst.Config) {
var id state.ID
if err := state.NewAppID(&id); err != nil {
log.Fatal(err)
}
seal := outcome{syscallDispatcher: direct{}}
if err := seal.finalise(ctx, msg, &id, config); err != nil {
seal := outcome{id: &stringPair[state.ID]{id, id.String()}, syscallDispatcher: direct{}}
if err := seal.finalise(ctx, config); err != nil {
printMessageError("cannot seal app:", err)
os.Exit(1)
}
seal.main(msg)
seal.main()
panic("unreachable")
}

View File

@ -0,0 +1,199 @@
package app
import (
"fmt"
"io/fs"
"log"
"os/exec"
"os/user"
)
type stubNixOS struct {
lookPathErr map[string]error
usernameErr map[string]error
}
func (k *stubNixOS) new(func(k syscallDispatcher)) { panic("not implemented") }
func (k *stubNixOS) getuid() int { return 1971 }
func (k *stubNixOS) getgid() int { return 100 }
func (k *stubNixOS) lookupEnv(key string) (string, bool) {
switch key {
case "SHELL":
return "/run/current-system/sw/bin/zsh", true
case "TERM":
return "xterm-256color", true
case "WAYLAND_DISPLAY":
return "wayland-0", true
case "PULSE_COOKIE":
return "", false
case "HOME":
return "/home/ophestra", true
case "XDG_RUNTIME_DIR":
return "/run/user/1971", true
case "XDG_CONFIG_HOME":
return "/home/ophestra/xdg/config", true
default:
panic(fmt.Sprintf("attempted to access unexpected environment variable %q", key))
}
}
func (k *stubNixOS) stat(name string) (fs.FileInfo, error) {
switch name {
case "/var/run/nscd":
return nil, nil
case "/run/user/1971/pulse":
return nil, nil
case "/run/user/1971/pulse/native":
return stubFileInfoMode(0666), nil
case "/home/ophestra/.pulse-cookie":
return stubFileInfoIsDir(true), nil
case "/home/ophestra/xdg/config/pulse/cookie":
return stubFileInfoIsDir(false), nil
default:
panic(fmt.Sprintf("attempted to stat unexpected path %q", name))
}
}
func (k *stubNixOS) readdir(name string) ([]fs.DirEntry, error) {
switch name {
case "/":
return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib",
"lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
case "/run":
return stubDirEntries("agetty.reload", "binfmt", "booted-system",
"credentials", "cryptsetup", "current-system", "dbus", "host", "keys",
"libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager",
"nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm",
"store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2",
"user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state")
case "/etc":
return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default",
"ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid",
"hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd",
"libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm",
"machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc",
"netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf",
"opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1",
"profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba",
"sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh",
"ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc",
"zoneinfo", "zprofile", "zshenv", "zshrc")
default:
panic(fmt.Sprintf("attempted to read unexpected directory %q", name))
}
}
func (k *stubNixOS) tempdir() string { return "/tmp/" }
func (k *stubNixOS) evalSymlinks(path string) (string, error) {
switch path {
case "/run/user/1971":
return "/run/user/1971", nil
case "/tmp/hakurei.0":
return "/tmp/hakurei.0", nil
case "/run/dbus":
return "/run/dbus", nil
case "/dev/kvm":
return "/dev/kvm", nil
case "/etc/":
return "/etc/", nil
case "/bin":
return "/bin", nil
case "/boot":
return "/boot", nil
case "/home":
return "/home", nil
case "/lib":
return "/lib", nil
case "/lib64":
return "/lib64", nil
case "/nix":
return "/nix", nil
case "/root":
return "/root", nil
case "/run":
return "/run", nil
case "/srv":
return "/srv", nil
case "/sys":
return "/sys", nil
case "/usr":
return "/usr", nil
case "/var":
return "/var", nil
case "/dev/dri":
return "/dev/dri", nil
case "/usr/bin/":
return "/usr/bin/", nil
case "/nix/store":
return "/nix/store", nil
case "/run/current-system":
return "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-nixos-system-satori-25.05.99999999.aaaaaaa", nil
case "/sys/block":
return "/sys/block", nil
case "/sys/bus":
return "/sys/bus", nil
case "/sys/class":
return "/sys/class", nil
case "/sys/dev":
return "/sys/dev", nil
case "/sys/devices":
return "/sys/devices", nil
case "/run/opengl-driver":
return "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-graphics-drivers", nil
case "/var/lib/persist/module/hakurei/0/1":
return "/var/lib/persist/module/hakurei/0/1", nil
default:
panic(fmt.Sprintf("attempted to evaluate unexpected path %q", path))
}
}
func (k *stubNixOS) lookPath(file string) (string, error) {
if k.lookPathErr != nil {
if err, ok := k.lookPathErr[file]; ok {
return "", err
}
}
switch file {
case "zsh":
return "/run/current-system/sw/bin/zsh", nil
default:
panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
}
}
func (k *stubNixOS) lookupGroupId(name string) (string, error) {
switch name {
case "video":
return "26", nil
default:
return "", user.UnknownGroupError(name)
}
}
func (k *stubNixOS) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
switch cmd.Path {
case "/proc/nonexistent/hsu":
return []byte{'0'}, nil
default:
panic(fmt.Sprintf("unexpected cmd %#v", cmd))
}
}
func (k *stubNixOS) overflowUid() int { return 65534 }
func (k *stubNixOS) overflowGid() int { return 65534 }
func (k *stubNixOS) mustHsuPath() string { return "/proc/nonexistent/hsu" }
func (k *stubNixOS) fatalf(format string, v ...any) { panic(fmt.Sprintf(format, v...)) }
func (k *stubNixOS) isVerbose() bool { return true }
func (k *stubNixOS) verbose(v ...any) { log.Print(v...) }
func (k *stubNixOS) verbosef(format string, v ...any) { log.Printf(format, v...) }

View File

@ -1,36 +1,25 @@
package app
import (
"bytes"
"encoding/gob"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log"
"maps"
"os/exec"
"os/user"
"os"
"reflect"
"syscall"
"testing"
"time"
"hakurei.app/container"
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"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) {
msg := container.NewMsg(nil)
msg.SwapVerbose(testing.Verbose())
testCases := []struct {
name string
k syscallDispatcher
@ -41,47 +30,19 @@ func TestApp(t *testing.T) {
}{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&hst.Config{Container: &hst.ContainerConfig{
Userns: true, HostNet: true, HostAbstract: true, Tty: true,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsRoot,
Source: fhs.AbsRoot,
Write: true,
Special: true,
}},
{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("kvm"),
Device: true,
Optional: true,
}},
{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsEtc,
Source: fhs.AbsEtc,
Special: true,
}},
},
Username: "chronos",
Shell: m("/run/current-system/sw/bin/zsh"),
Home: m("/home/chronos"),
Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"/run/current-system/sw/bin/zsh"},
}},
&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(t.Context(), msg, 1000000).
Ensure(m("/tmp/hakurei.0"), 0711).
Ensure(m("/tmp/hakurei.0/runtime"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
Ensure(m("/tmp/hakurei.0/runtime/0"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/0"), acl.Read, acl.Write, acl.Execute).
Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
Ensure(m("/tmp/hakurei.0/tmpdir/0"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/0"), acl.Read, acl.Write, acl.Execute),
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"),
@ -96,24 +57,24 @@ func TestApp(t *testing.T) {
"XDG_SESSION_TYPE=tty",
},
Ops: new(container.Ops).
Root(m("/"), bits.BindWritable).
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"), bits.BindWritable|bits.BindDevice|bits.BindOptional).
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/nscd"), 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"), bits.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), bits.BindWritable).
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: bits.PresetExt | bits.PresetDenyDevel,
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true,
HostAbstract: true,
RetainSession: true,
@ -124,9 +85,12 @@ func TestApp(t *testing.T) {
"nixos permissive defaults chromium", new(stubNixOS),
&hst.Config{
ID: "org.chromium.Chromium",
Args: []string{"zsh", "-c", "exec chromium "},
Identity: 9,
Groups: []string{"video"},
SessionBus: &hst.BusConfig{
Username: "chronos",
Home: m("/home/chronos"),
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
@ -149,7 +113,7 @@ func TestApp(t *testing.T) {
},
Filter: true,
},
SystemBus: &hst.BusConfig{
SystemBus: &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
@ -157,42 +121,7 @@ func TestApp(t *testing.T) {
},
Filter: true,
},
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
Container: &hst.ContainerConfig{
Userns: true, HostNet: true, HostAbstract: true, Tty: true,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsRoot,
Source: fhs.AbsRoot,
Write: true,
Special: true,
}},
{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("dri"),
Device: true,
Optional: true,
}},
{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("kvm"),
Device: true,
Optional: true,
}},
{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsEtc,
Source: fhs.AbsEtc,
Special: true,
}},
},
Username: "chronos",
Shell: m("/run/current-system/sw/bin/zsh"),
Home: m("/home/chronos"),
Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"zsh", "-c", "exec chromium "},
},
Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse),
},
state.ID{
0xeb, 0xf0, 0x83, 0xd1,
@ -200,19 +129,20 @@ func TestApp(t *testing.T) {
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(t.Context(), msg, 1000009).
Ensure(m("/tmp/hakurei.0"), 0711).
Ensure(m("/tmp/hakurei.0/runtime"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
Ensure(m("/tmp/hakurei.0/runtime/9"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute).
Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711).
Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/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, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), acl.Execute).
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse")).
MustProxyDBus(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), &hst.BusConfig{
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",
@ -234,7 +164,7 @@ func TestApp(t *testing.T) {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
}, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), &hst.BusConfig{
}, "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
@ -242,8 +172,8 @@ func TestApp(t *testing.T) {
},
Filter: true,
}).
UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), acl.Read, acl.Write).
UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), acl.Read, acl.Write),
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"),
@ -263,30 +193,30 @@ func TestApp(t *testing.T) {
"XDG_SESSION_TYPE=tty",
},
Ops: new(container.Ops).
Root(m("/"), bits.BindWritable).
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"), bits.BindWritable|bits.BindDevice|bits.BindOptional).
Bind(m("/dev/kvm"), m("/dev/kvm"), bits.BindWritable|bits.BindDevice|bits.BindOptional).
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/nscd"), 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"), bits.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/9"), m("/tmp/"), bits.BindWritable).
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"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
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: bits.PresetExt | bits.PresetDenyDevel,
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true,
HostAbstract: true,
RetainSession: true,
@ -298,7 +228,10 @@ func TestApp(t *testing.T) {
"nixos chromium direct wayland", new(stubNixOS),
&hst.Config{
ID: "org.chromium.Chromium",
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
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{
@ -316,18 +249,12 @@ func TestApp(t *testing.T) {
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}),
},
Username: "u0_a1",
Shell: m("/run/current-system/sw/bin/zsh"),
Home: m("/var/lib/persist/module/hakurei/0/1"),
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
},
SystemBus: &hst.BusConfig{
SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Filter: true,
},
SessionBus: &hst.BusConfig{
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
@ -343,6 +270,8 @@ func TestApp(t *testing.T) {
},
DirectWayland: true,
Username: "u0_a1",
Home: m("/var/lib/persist/module/hakurei/0/1"),
Identity: 1, Groups: []string{},
},
state.ID{
@ -351,19 +280,20 @@ func TestApp(t *testing.T) {
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
},
system.New(t.Context(), msg, 1000001).
Ensure(m("/tmp/hakurei.0"), 0711).
Ensure(m("/tmp/hakurei.0/runtime"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
Ensure(m("/tmp/hakurei.0/runtime/1"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/1"), acl.Read, acl.Write, acl.Execute).
Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
Ensure(m("/tmp/hakurei.0/tmpdir/1"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/1"), acl.Read, acl.Write, acl.Execute).
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), acl.Execute).
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse")).
Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711).
MustProxyDBus(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), &hst.BusConfig{
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",
@ -376,7 +306,7 @@ func TestApp(t *testing.T) {
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
}, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), &hst.BusConfig{
}, "/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
@ -384,8 +314,8 @@ func TestApp(t *testing.T) {
},
Filter: true,
}).
UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), acl.Read, acl.Write).
UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), acl.Read, acl.Write),
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,
@ -415,28 +345,28 @@ func TestApp(t *testing.T) {
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"), bits.BindOptional).
Bind(m("/sys/bus"), m("/sys/bus"), bits.BindOptional).
Bind(m("/sys/class"), m("/sys/class"), bits.BindOptional).
Bind(m("/sys/dev"), m("/sys/dev"), bits.BindOptional).
Bind(m("/sys/devices"), m("/sys/devices"), bits.BindOptional).
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"), bits.BindDevice|bits.BindWritable|bits.BindOptional).
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"), bits.BindWritable|bits.BindEnsure).
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"), bits.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), bits.BindWritable).
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"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
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: bits.PresetExt | bits.PresetDenyTTY | bits.PresetDenyDevel,
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel,
HostNet: true,
ForwardCancel: true,
},
@ -445,77 +375,30 @@ func TestApp(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gr, gw := io.Pipe()
var gotSys *system.I
{
sPriv := outcomeState{
ID: &tc.id,
Identity: tc.config.Identity,
UserID: (&Hsu{k: tc.k}).MustIDMsg(msg),
EnvPaths: copyPaths(tc.k),
Container: tc.config.Container,
}
sPriv.populateEarly(tc.k, msg, tc.config)
if err := sPriv.populateLocal(tc.k, msg); err != nil {
t.Fatalf("populateLocal: error = %#v", err)
}
gotSys = system.New(t.Context(), msg, sPriv.uid.unwrap())
stateSys := outcomeStateSys{sys: gotSys, outcomeState: &sPriv}
for _, op := range sPriv.Shim.Ops {
if err := op.toSystem(&stateSys, tc.config); err != nil {
t.Fatalf("toSystem: error = %#v", err)
}
}
go func() {
e := gob.NewEncoder(gw)
if err := errors.Join(e.Encode(&sPriv)); err != nil {
t.Errorf("Encode: error = %v", err)
panic("unexpected encode fault")
}
}()
}
var gotParams container.Params
{
var sShim outcomeState
d := gob.NewDecoder(gr)
if err := errors.Join(d.Decode(&sShim)); err != nil {
t.Fatalf("Decode: error = %v", err)
}
if err := sShim.populateLocal(tc.k, msg); err != nil {
t.Fatalf("populateLocal: error = %#v", err)
}
stateParams := outcomeStateParams{params: &gotParams, outcomeState: &sShim}
if sShim.Container.Env == nil {
stateParams.env = make(map[string]string, envAllocSize)
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 {
stateParams.env = maps.Clone(sShim.Container.Env)
}
for _, op := range sShim.Shim.Ops {
if err := op.toContainer(&stateParams); err != nil {
t.Fatalf("toContainer: error = %#v", err)
}
t.Fatalf("Seal: %s", s)
}
}
t.Run("sys", func(t *testing.T) {
if !gotSys.Equal(tc.wantSys) {
t.Errorf("toSystem: sys = %#v, want %#v", gotSys, tc.wantSys)
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(&gotParams, tc.wantParams) {
t.Errorf("toContainer: params =\n%s\n, want\n%s", mustMarshal(&gotParams), mustMarshal(tc.wantParams))
if !reflect.DeepEqual(seal.container, tc.wantParams) {
t.Errorf("seal: container =\n%s\n, want\n%s", mustMarshal(seal.container), mustMarshal(tc.wantParams))
}
})
})
})
}
}
@ -560,203 +443,8 @@ func (s stubFileInfoIsDir) ModTime() time.Time { panic("attempted to call ModTim
func (s stubFileInfoIsDir) IsDir() bool { return bool(s) }
func (s stubFileInfoIsDir) Sys() any { panic("attempted to call Sys") }
type stubFileInfoPulseCookie struct{ stubFileInfoIsDir }
func (s stubFileInfoPulseCookie) Size() int64 { return pulseCookieSizeMax }
type stubOsFileReadCloser struct{ io.ReadCloser }
func (s stubOsFileReadCloser) Name() string { panic("attempting to call Name") }
func (s stubOsFileReadCloser) Write([]byte) (int, error) { panic("attempting to call Write") }
func (s stubOsFileReadCloser) Stat() (fs.FileInfo, error) { panic("attempting to call Stat") }
type stubNixOS struct {
usernameErr map[string]error
}
func (k *stubNixOS) new(func(k syscallDispatcher)) { panic("not implemented") }
func (k *stubNixOS) getuid() int { return 1971 }
func (k *stubNixOS) getgid() int { return 100 }
func (k *stubNixOS) lookupEnv(key string) (string, bool) {
switch key {
case "SHELL":
return "/run/current-system/sw/bin/zsh", true
case "TERM":
return "xterm-256color", true
case "WAYLAND_DISPLAY":
return "wayland-0", true
case "PULSE_COOKIE":
return "", false
case "HOME":
return "/home/ophestra", true
case "XDG_RUNTIME_DIR":
return "/run/user/1971", true
case "XDG_CONFIG_HOME":
return "/home/ophestra/xdg/config", true
default:
panic(fmt.Sprintf("attempted to access unexpected environment variable %q", key))
}
}
func (k *stubNixOS) stat(name string) (fs.FileInfo, error) {
switch name {
case "/var/run/nscd":
return nil, nil
case "/run/user/1971/pulse":
return nil, nil
case "/run/user/1971/pulse/native":
return stubFileInfoMode(0666), nil
case "/home/ophestra/.pulse-cookie":
return stubFileInfoIsDir(true), nil
case "/home/ophestra/xdg/config/pulse/cookie":
return stubFileInfoPulseCookie{false}, nil
default:
panic(fmt.Sprintf("attempted to stat unexpected path %q", name))
}
}
func (k *stubNixOS) open(name string) (osFile, error) {
switch name {
case "/home/ophestra/xdg/config/pulse/cookie":
return stubOsFileReadCloser{io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{0}, pulseCookieSizeMax)))}, nil
default:
panic(fmt.Sprintf("attempted to open unexpected path %q", name))
}
}
func (k *stubNixOS) readdir(name string) ([]fs.DirEntry, error) {
switch name {
case "/":
return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib",
"lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
case "/run":
return stubDirEntries("agetty.reload", "binfmt", "booted-system",
"credentials", "cryptsetup", "current-system", "dbus", "host", "keys",
"libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager",
"nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm",
"store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2",
"user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state")
case "/etc":
return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default",
"ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid",
"hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd",
"libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm",
"machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc",
"netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf",
"opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1",
"profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba",
"sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh",
"ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc",
"zoneinfo", "zprofile", "zshenv", "zshrc")
default:
panic(fmt.Sprintf("attempted to read unexpected directory %q", name))
}
}
func (k *stubNixOS) tempdir() string { return "/tmp/" }
func (k *stubNixOS) evalSymlinks(path string) (string, error) {
switch path {
case "/var/run/nscd":
return "/run/nscd", nil
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) 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(container.Msg) int { return 65534 }
func (k *stubNixOS) overflowGid(container.Msg) int { return 65534 }
func (k *stubNixOS) mustHsuPath() *check.Absolute { return m("/proc/nonexistent/hsu") }
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...) }
func m(pathname string) *check.Absolute {
return check.MustAbs(pathname)
func m(pathname string) *container.Absolute {
return container.MustAbs(pathname)
}
func f(c hst.FilesystemConfig) hst.FilesystemConfigJSON {

254
internal/app/container.go Normal file
View File

@ -0,0 +1,254 @@
package app
import (
"errors"
"fmt"
"io/fs"
"maps"
"path"
"syscall"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/system/dbus"
)
// in practice there should be less than 30 system mount points
const preallocateOpsCount = 1 << 5
// newContainer initialises [container.Params] via [hst.ContainerConfig].
// Note that remaining container setup must be queued by the caller.
func newContainer(
k syscallDispatcher,
s *hst.ContainerConfig,
prefix string,
sc *hst.Paths,
uid, gid *int,
) (*container.Params, map[string]string, error) {
if s == nil {
return nil, nil, newWithMessage("invalid container configuration")
}
params := &container.Params{
Hostname: s.Hostname,
SeccompFlags: s.SeccompFlags,
SeccompPresets: s.SeccompPresets,
RetainSession: s.Tty,
HostNet: s.HostNet,
HostAbstract: s.HostAbstract,
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
// this behaviour is implemented in the shim
ForwardCancel: s.WaitDelay >= 0,
}
as := &hst.ApplyState{AutoEtcPrefix: prefix}
{
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem))
params.Ops = &ops
as.Ops = &ops
}
if s.Multiarch {
params.SeccompFlags |= seccomp.AllowMultiarch
}
if !s.SeccompCompat {
params.SeccompPresets |= seccomp.PresetExt
}
if !s.Devel {
params.SeccompPresets |= seccomp.PresetDenyDevel
}
if !s.Userns {
params.SeccompPresets |= seccomp.PresetDenyNS
}
if !s.Tty {
params.SeccompPresets |= seccomp.PresetDenyTTY
}
if s.MapRealUID {
params.Uid = k.getuid()
*uid = params.Uid
params.Gid = k.getgid()
*gid = params.Gid
} else {
*uid = k.overflowUid()
*gid = k.overflowGid()
}
filesystem := s.Filesystem
var autoroot *hst.FSBind
// valid happens late, so root mount gets it here
if len(filesystem) > 0 && filesystem[0].Valid() && filesystem[0].Path().String() == container.FHSRoot {
// if the first element targets /, it is inserted early and excluded from path hiding
rootfs := filesystem[0].FilesystemConfig
filesystem = filesystem[1:]
rootfs.Apply(as)
// autoroot requires special handling during path hiding
if b, ok := rootfs.(*hst.FSBind); ok && b.IsAutoRoot() {
autoroot = b
}
}
params.
Proc(container.AbsFHSProc).
Tmpfs(hst.AbsTmp, 1<<12, 0755)
if !s.Device {
params.DevWritable(container.AbsFHSDev, true)
} else {
params.Bind(container.AbsFHSDev, container.AbsFHSDev, container.BindWritable|container.BindDevice)
}
// /dev is mounted readonly later on, this prevents /dev/shm from going readonly with it
params.Tmpfs(container.AbsFHSDev.Append("shm"), 0, 01777)
/* retrieve paths and hide them if they're made available in the sandbox;
this feature tries to improve user experience of permissive defaults, and
to warn about issues in custom configuration; it is NOT a security feature
and should not be treated as such, ALWAYS be careful with what you bind */
var hidePaths []string
hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String())
_, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
return nil, nil, err
} else {
// there is usually only one, do not preallocate
for _, entry := range entries {
if entry.Method != "unix" {
continue
}
for _, pair := range entry.Values {
if pair[0] == "path" {
if path.IsAbs(pair[1]) {
// get parent dir of socket
dir := path.Dir(pair[1])
if dir == "." || dir == container.FHSRoot {
k.verbosef("dbus socket %q is in an unusual location", pair[1])
}
hidePaths = append(hidePaths, dir)
} else {
k.verbosef("dbus socket %q is not absolute", pair[1])
}
}
}
}
}
hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths {
if err := evalSymlinks(k, &hidePaths[i]); err != nil {
return nil, nil, err
}
}
var hidePathSourceCount int
for i, c := range filesystem {
if !c.Valid() {
return nil, nil, fmt.Errorf("invalid filesystem at index %d", i)
}
c.Apply(as)
// fs counter
hidePathSourceCount += len(c.Host())
}
// AutoRootOp is a collection of many BindMountOp internally
var autoRootEntries []fs.DirEntry
if autoroot != nil {
if d, err := k.readdir(autoroot.Source.String()); err != nil {
return nil, nil, err
} else {
// autoroot counter
hidePathSourceCount += len(d)
autoRootEntries = d
}
}
hidePathSource := make([]*container.Absolute, 0, hidePathSourceCount)
// fs append
for _, c := range filesystem {
// all entries already checked above
hidePathSource = append(hidePathSource, c.Host()...)
}
// autoroot append
if autoroot != nil {
for _, ent := range autoRootEntries {
name := ent.Name()
if container.IsAutoRootBindable(name) {
hidePathSource = append(hidePathSource, autoroot.Source.Append(name))
}
}
}
// evaluated path, input path
hidePathSourceEval := make([][2]string, len(hidePathSource))
for i, a := range hidePathSource {
if a == nil {
// unreachable
return nil, nil, syscall.ENOTRECOVERABLE
}
hidePathSourceEval[i] = [2]string{a.String(), a.String()}
if err := evalSymlinks(k, &hidePathSourceEval[i][0]); err != nil {
return nil, nil, err
}
}
for _, p := range hidePathSourceEval {
for i := range hidePaths {
// skip matched entries
if hidePathMatch[i] {
continue
}
if ok, err := deepContainsH(p[0], hidePaths[i]); err != nil {
return nil, nil, err
} else if ok {
hidePathMatch[i] = true
k.verbosef("hiding path %q from %q", hidePaths[i], p[1])
}
}
}
// cover matched paths
for i, ok := range hidePathMatch {
if ok {
if a, err := container.NewAbs(hidePaths[i]); err != nil {
var absoluteError *container.AbsoluteError
if !errors.As(err, &absoluteError) {
return nil, nil, err
}
if absoluteError == nil {
return nil, nil, syscall.ENOTRECOVERABLE
}
return nil, nil, fmt.Errorf("invalid path hiding candidate %q", absoluteError.Pathname)
} else {
params.Tmpfs(a, 1<<13, 0755)
}
}
}
// no more ContainerConfig paths beyond this point
if !s.Device {
params.Remount(container.AbsFHSDev, syscall.MS_RDONLY)
}
return params, maps.Clone(s.Env), nil
}
// evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist].
func evalSymlinks(k syscallDispatcher, v *string) error {
if p, err := k.evalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
k.verbosef("path %q does not yet exist", *v)
} else {
*v = p
}
return nil
}

View File

@ -1,8 +1,6 @@
package app
import (
"io"
"io/fs"
"log"
"os"
"os/exec"
@ -10,17 +8,10 @@ import (
"path/filepath"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
// osFile represents [os.File].
type osFile interface {
Name() string
io.Writer
fs.File
}
// 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.
@ -36,8 +27,6 @@ type syscallDispatcher interface {
lookupEnv(key string) (string, bool)
// stat provides [os.Stat].
stat(name string) (os.FileInfo, error)
// open provides [os.Open].
open(name string) (osFile, error)
// readdir provides [os.ReadDir].
readdir(name string) ([]os.DirEntry, error)
// tempdir provides [os.TempDir].
@ -46,6 +35,9 @@ type syscallDispatcher interface {
// 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)
@ -53,15 +45,19 @@ type syscallDispatcher interface {
cmdOutput(cmd *exec.Cmd) ([]byte, error)
// overflowUid provides [container.OverflowUid].
overflowUid(msg container.Msg) int
overflowUid() int
// overflowGid provides [container.OverflowGid].
overflowGid(msg container.Msg) int
overflowGid() int
// mustHsuPath provides [internal.MustHsuPath].
mustHsuPath() *check.Absolute
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.
@ -73,12 +69,13 @@ 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) open(name string) (osFile, error) { return os.Open(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)
@ -90,9 +87,13 @@ func (direct) lookupGroupId(name string) (gid string, err error) {
func (direct) cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() }
func (direct) overflowUid(msg container.Msg) int { return container.OverflowUid(msg) }
func (direct) overflowGid(msg container.Msg) int { return container.OverflowGid(msg) }
func (direct) overflowUid() int { return container.OverflowUid() }
func (direct) overflowGid() int { return container.OverflowGid() }
func (direct) mustHsuPath() *check.Absolute { return internal.MustHsuPath() }
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...) }

View File

@ -1,27 +0,0 @@
package app
import (
"os"
"os/exec"
"hakurei.app/container"
"hakurei.app/container/check"
)
type panicDispatcher struct{}
func (panicDispatcher) new(func(k syscallDispatcher)) { panic("unreachable") }
func (panicDispatcher) getuid() int { panic("unreachable") }
func (panicDispatcher) getgid() int { panic("unreachable") }
func (panicDispatcher) lookupEnv(string) (string, bool) { panic("unreachable") }
func (panicDispatcher) stat(string) (os.FileInfo, error) { panic("unreachable") }
func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") }
func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
func (panicDispatcher) tempdir() string { panic("unreachable") }
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
func (panicDispatcher) overflowUid(container.Msg) int { panic("unreachable") }
func (panicDispatcher) overflowGid(container.Msg) int { panic("unreachable") }
func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") }
func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") }

View File

@ -1,59 +0,0 @@
package app
import (
"strconv"
"hakurei.app/container/check"
"hakurei.app/hst"
)
// EnvPaths holds paths copied from the environment and is used to create [hst.Paths].
type EnvPaths struct {
// TempDir is returned by [os.TempDir].
TempDir *check.Absolute
// RuntimePath is copied from $XDG_RUNTIME_DIR.
RuntimePath *check.Absolute
}
// Copy expands [EnvPaths] into [hst.Paths].
func (env *EnvPaths) Copy(v *hst.Paths, userid int) {
if env == nil || env.TempDir == nil || v == nil {
panic("attempting to use an invalid EnvPaths")
}
v.TempDir = env.TempDir
v.SharePath = env.TempDir.Append("hakurei." + strconv.Itoa(userid))
if env.RuntimePath == 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 = env.RuntimePath
v.RunDirPath = env.RuntimePath.Append("hakurei")
}
}
// CopyPaths returns a populated [EnvPaths].
func CopyPaths() *EnvPaths { return copyPaths(direct{}) }
// copyPaths returns a populated [EnvPaths].
func copyPaths(k syscallDispatcher) *EnvPaths {
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
var env EnvPaths
if tempDir, err := check.NewAbs(k.tempdir()); err != nil {
k.fatalf("invalid TMPDIR: %v", err)
panic("unreachable")
} else {
env.TempDir = tempDir
}
r, _ := k.lookupEnv(xdgRuntimeDir)
if a, err := check.NewAbs(r); err == nil {
env.RuntimePath = a
}
return &env
}

View File

@ -1,131 +0,0 @@
package app
import (
"fmt"
"reflect"
"testing"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/stub"
"hakurei.app/hst"
)
func TestEnvPaths(t *testing.T) {
testCases := []struct {
name string
env *EnvPaths
want hst.Paths
wantPanic string
}{
{"nil", nil, hst.Paths{}, "attempting to use an invalid EnvPaths"},
{"zero", new(EnvPaths), hst.Paths{}, "attempting to use an invalid EnvPaths"},
{"nil tempdir", &EnvPaths{
RuntimePath: fhs.AbsTmp,
}, hst.Paths{}, "attempting to use an invalid EnvPaths"},
{"nil runtime", &EnvPaths{
TempDir: fhs.AbsTmp,
}, hst.Paths{
TempDir: fhs.AbsTmp,
SharePath: fhs.AbsTmp.Append("hakurei.3735928559"),
RuntimePath: fhs.AbsTmp.Append("hakurei.3735928559/run/compat"),
RunDirPath: fhs.AbsTmp.Append("hakurei.3735928559/run"),
}, ""},
{"full", &EnvPaths{
TempDir: fhs.AbsTmp,
RuntimePath: fhs.AbsRunUser.Append("1000"),
}, hst.Paths{
TempDir: fhs.AbsTmp,
SharePath: fhs.AbsTmp.Append("hakurei.3735928559"),
RuntimePath: fhs.AbsRunUser.Append("1000"),
RunDirPath: fhs.AbsRunUser.Append("1000/hakurei"),
}, ""},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.wantPanic != "" {
defer func() {
if r := recover(); r != tc.wantPanic {
t.Errorf("Copy: panic = %#v, want %q", r, tc.wantPanic)
}
}()
}
var sc hst.Paths
tc.env.Copy(&sc, 0xdeadbeef)
if !reflect.DeepEqual(&sc, &tc.want) {
t.Errorf("Copy: %#v, want %#v", sc, tc.want)
}
})
}
}
func TestCopyPaths(t *testing.T) {
testCases := []struct {
name string
env map[string]string
tmp string
fatal string
want EnvPaths
}{
{"invalid tempdir", nil, "\x00",
"invalid TMPDIR: path \"\\x00\" is not absolute", EnvPaths{}},
{"empty environment", make(map[string]string), container.Nonexistent,
"", EnvPaths{TempDir: check.MustAbs(container.Nonexistent)}},
{"invalid XDG_RUNTIME_DIR", map[string]string{"XDG_RUNTIME_DIR": "\x00"}, container.Nonexistent,
"", EnvPaths{TempDir: check.MustAbs(container.Nonexistent)}},
{"full", map[string]string{"XDG_RUNTIME_DIR": "/\x00"}, container.Nonexistent,
"", EnvPaths{TempDir: check.MustAbs(container.Nonexistent), RuntimePath: check.MustAbs("/\x00")}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.fatal != "" {
defer stub.HandleExit(t)
}
k := copyPathsDispatcher{t: t, env: tc.env, tmp: tc.tmp, expectsFatal: tc.fatal}
got := copyPaths(k)
if tc.fatal != "" {
t.Fatalf("copyPaths: expected fatal %q", tc.fatal)
}
if !reflect.DeepEqual(got, &tc.want) {
t.Errorf("copyPaths: %#v, want %#v", got, &tc.want)
}
})
}
}
// copyPathsDispatcher implements enough of syscallDispatcher for all copyPaths code paths.
type copyPathsDispatcher struct {
env map[string]string
tmp string
// must be checked at the conclusion of the test
expectsFatal string
t *testing.T
panicDispatcher
}
func (k copyPathsDispatcher) tempdir() string { return k.tmp }
func (k copyPathsDispatcher) lookupEnv(key string) (value string, ok bool) {
value, ok = k.env[key]
return
}
func (k copyPathsDispatcher) fatalf(format string, v ...any) {
if k.expectsFatal == "" {
k.t.Fatalf("unexpected call to fatalf: format = %q, v = %#v", format, v)
}
if got := fmt.Sprintf(format, v...); got != k.expectsFatal {
k.t.Fatalf("fatalf: %q, want %q", got, k.expectsFatal)
}
panic(stub.PanicExit)
}

View File

@ -7,14 +7,24 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/user"
"slices"
"strconv"
"strings"
"sync/atomic"
"syscall"
"time"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system"
"hakurei.app/system/acl"
"hakurei.app/system/dbus"
"hakurei.app/system/wayland"
)
func newWithMessage(msg string) error { return newWithMessageError(msg, os.ErrInvalid) }
@ -24,40 +34,129 @@ func newWithMessageError(msg string, err error) error {
// An outcome is the runnable state of a hakurei container via [hst.Config].
type outcome struct {
// copied from initialising [app]
id *stringPair[state.ID]
// copied from [sys.State]
runDirPath *container.Absolute
// initial [hst.Config] gob stream for state data;
// this is prepared ahead of time as config is clobbered during seal creation
ct io.WriterTo
// dump dbus proxy message buffer
dbusMsg func()
// Supplementary group ids. Populated during finalise.
supp []string
// Resolved priv side operating system interactions. Populated during finalise.
user hsuUser
sys *system.I
// Transmitted to shim. Populated during finalise.
state *outcomeState
ctx context.Context
// Whether the current process is in outcome.main.
waitDelay time.Duration
container *container.Params
env map[string]string
sync *os.File
active atomic.Bool
ctx context.Context
syscallDispatcher
}
func (k *outcome) finalise(ctx context.Context, msg container.Msg, id *state.ID, config *hst.Config) error {
if ctx == nil || id == nil {
// shareHost holds optional share directory state that must not be accessed directly
type shareHost struct {
// whether XDG_RUNTIME_DIR is used post hsu
useRuntimeDir bool
// process-specific directory in tmpdir, empty if unused
sharePath *container.Absolute
// process-specific directory in XDG_RUNTIME_DIR, empty if unused
runtimeSharePath *container.Absolute
seal *outcome
sc hst.Paths
}
// ensureRuntimeDir must be called if direct access to paths within XDG_RUNTIME_DIR is required
func (share *shareHost) ensureRuntimeDir() {
if share.useRuntimeDir {
return
}
share.useRuntimeDir = true
share.seal.sys.Ensure(share.sc.RunDirPath.String(), 0700)
share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath.String(), acl.Execute)
share.seal.sys.Ensure(share.sc.RuntimePath.String(), 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath.String(), acl.Execute)
}
// instance returns a process-specific share path within tmpdir
func (share *shareHost) instance() *container.Absolute {
if share.sharePath != nil {
return share.sharePath
}
share.sharePath = share.sc.SharePath.Append(share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.sharePath.String(), 0711)
return share.sharePath
}
// runtime returns a process-specific share path within XDG_RUNTIME_DIR
func (share *shareHost) runtime() *container.Absolute {
if share.runtimeSharePath != nil {
return share.runtimeSharePath
}
share.ensureRuntimeDir()
share.runtimeSharePath = share.sc.RunDirPath.Append(share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath.String(), 0700)
share.seal.sys.UpdatePerm(share.runtimeSharePath.String(), acl.Execute)
return share.runtimeSharePath
}
// hsuUser stores post-hsu credentials and metadata
type hsuUser struct {
identity *stringPair[int]
// target uid resolved by hid:aid
uid *stringPair[int]
// supplementary group ids
supp []string
// app user home directory
home *container.Absolute
// passwd database username
username string
}
func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
const (
home = "HOME"
shell = "SHELL"
xdgConfigHome = "XDG_CONFIG_HOME"
xdgRuntimeDir = "XDG_RUNTIME_DIR"
xdgSessionClass = "XDG_SESSION_CLASS"
xdgSessionType = "XDG_SESSION_TYPE"
term = "TERM"
display = "DISPLAY"
pulseServer = "PULSE_SERVER"
pulseCookie = "PULSE_COOKIE"
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
)
if ctx == nil {
// unreachable
panic("invalid call to finalise")
}
if k.ctx != nil || k.sys != nil || k.state != nil {
if k.ctx != nil {
// unreachable
panic("attempting to finalise twice")
}
k.ctx = ctx
if err := config.Validate(); err != nil {
return err
if config == nil {
return newWithMessage("invalid configuration")
}
if config.Home == nil {
return newWithMessage("invalid path to home directory")
}
// TODO(ophestra): do not clobber during finalise
{
// encode initial configuration for state tracking
ct := new(bytes.Buffer)
@ -67,8 +166,26 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, id *state.ID,
k.ct = ct
}
// hsu expects numerical group ids
supp := make([]string, len(config.Groups))
// allowed identity range 0 to 9999, this is checked again in hsu
if config.Identity < 0 || config.Identity > 9999 {
return newWithMessage(fmt.Sprintf("identity %d out of range", config.Identity))
}
k.user = hsuUser{
identity: newInt(config.Identity),
home: config.Home,
username: config.Username,
}
hsu := Hsu{k: k}
if k.user.username == "" {
k.user.username = "chronos"
} else if !isValidUsername(k.user.username) {
return newWithMessage(fmt.Sprintf("invalid user name %q", k.user.username))
}
k.user.uid = newInt(HsuUid(hsu.MustID(), k.user.identity.unwrap()))
k.user.supp = make([]string, len(config.Groups))
for i, name := range config.Groups {
if gid, err := k.lookupGroupId(name); err != nil {
var unknownGroupError user.UnknownGroupError
@ -78,33 +195,411 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, id *state.ID,
return &hst.AppError{Step: "look up group by name", Err: err}
}
} else {
supp[i] = gid
k.user.supp[i] = gid
}
}
// early validation complete at this point
s := outcomeState{
ID: id,
Identity: config.Identity,
UserID: (&Hsu{k: k}).MustIDMsg(msg),
EnvPaths: copyPaths(k.syscallDispatcher),
Container: config.Container,
// permissive defaults
if config.Container == nil {
hlog.Verbose("container configuration not supplied, PROCEED WITH CAUTION")
if config.Shell == nil {
config.Shell = container.AbsFHSRoot.Append("bin", "sh")
s, _ := k.lookupEnv(shell)
if a, err := container.NewAbs(s); err == nil {
config.Shell = a
}
s.populateEarly(k.syscallDispatcher, msg, config)
if err := s.populateLocal(k.syscallDispatcher, msg); err != nil {
}
// hsu clears the environment so resolve paths early
if config.Path == nil {
if len(config.Args) > 0 {
if p, err := k.lookPath(config.Args[0]); err != nil {
return &hst.AppError{Step: "look up executable file", Err: err}
} else if config.Path, err = container.NewAbs(p); err != nil {
return newWithMessageError(err.Error(), err)
}
} else {
config.Path = config.Shell
}
}
conf := &hst.ContainerConfig{
Userns: true,
HostNet: true,
HostAbstract: true,
Tty: true,
Filesystem: []hst.FilesystemConfigJSON{
// autoroot, includes the home directory
{FilesystemConfig: &hst.FSBind{
Target: container.AbsFHSRoot,
Source: container.AbsFHSRoot,
Write: true,
Special: true,
}},
},
}
// bind GPU stuff
if config.Enablements.Unwrap()&(system.EX11|system.EWayland) != 0 {
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}})
}
// opportunistically bind kvm
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("kvm"), Device: true, Optional: true}})
// hide nscd from container if present
nscd := container.AbsFHSVar.Append("run/nscd")
if _, err := k.stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) {
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}})
}
// do autoetc last
conf.Filesystem = append(conf.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: container.AbsFHSEtc,
Source: container.AbsFHSEtc,
Special: true,
}},
)
config.Container = conf
}
// late nil checks for pd behaviour
if config.Shell == nil {
return newWithMessage("invalid shell path")
}
if config.Path == nil {
return newWithMessage("invalid program path")
}
// TODO(ophestra): revert this after params to shim
share := &shareHost{seal: k}
copyPaths(k.syscallDispatcher, &share.sc, hsu.MustID())
var mapuid, mapgid *stringPair[int]
{
var uid, gid int
var err error
k.container, k.env, err = newContainer(k, config.Container, k.id.String(), &share.sc, &uid, &gid)
k.waitDelay = config.Container.WaitDelay
if err != nil {
return &hst.AppError{Step: "initialise container configuration", Err: err}
}
if len(config.Args) == 0 {
config.Args = []string{config.Path.String()}
}
k.container.Path = config.Path
k.container.Args = config.Args
mapuid = newInt(uid)
mapgid = newInt(gid)
if k.env == nil {
k.env = make(map[string]string, 1<<6)
}
}
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String())
k.env[xdgRuntimeDir] = innerRuntimeDir.String()
k.env[xdgSessionClass] = "user"
k.env[xdgSessionType] = "tty"
k.runDirPath = share.sc.RunDirPath
k.sys = system.New(k.ctx, k.user.uid.unwrap())
k.sys.Ensure(share.sc.SharePath.String(), 0711)
{
runtimeDir := share.sc.SharePath.Append("runtime")
k.sys.Ensure(runtimeDir.String(), 0700)
k.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute)
runtimeDirInst := runtimeDir.Append(k.user.identity.String())
k.sys.Ensure(runtimeDirInst.String(), 0700)
k.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute)
k.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
k.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
}
{
tmpdir := share.sc.SharePath.Append("tmpdir")
k.sys.Ensure(tmpdir.String(), 0700)
k.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute)
tmpdirInst := tmpdir.Append(k.user.identity.String())
k.sys.Ensure(tmpdirInst.String(), 01700)
k.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute)
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
k.container.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable)
}
{
username := "chronos"
if k.user.username != "" {
username = k.user.username
}
k.container.Dir = k.user.home
k.env["HOME"] = k.user.home.String()
k.env["USER"] = username
k.env[shell] = config.Shell.String()
k.container.Place(container.AbsFHSEtc.Append("passwd"),
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+k.user.home.String()+":"+config.Shell.String()+"\n"))
k.container.Place(container.AbsFHSEtc.Append("group"),
[]byte("hakurei:x:"+mapgid.String()+":\n"))
}
// pass TERM for proper terminal I/O in initial process
if t, ok := k.lookupEnv(term); ok {
k.env[term] = t
}
if config.Enablements.Unwrap()&system.EWayland != 0 {
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath *container.Absolute
if name, ok := k.lookupEnv(wayland.WaylandDisplay); !ok {
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
socketPath = share.sc.RuntimePath.Append(wayland.FallbackName)
} else if a, err := container.NewAbs(name); err != nil {
socketPath = share.sc.RuntimePath.Append(name)
} else {
socketPath = a
}
innerPath := innerRuntimeDir.Append(wayland.FallbackName)
k.env[wayland.WaylandDisplay] = wayland.FallbackName
if !config.DirectWayland { // set up security-context-v1
appID := config.ID
if appID == "" {
// use instance ID in case app id is not set
appID = "app.hakurei." + k.id.String()
}
// downstream socket paths
outerPath := share.instance().Append("wayland")
k.sys.Wayland(&k.sync, outerPath.String(), socketPath.String(), appID, k.id.String())
k.container.Bind(outerPath, innerPath, 0)
} else { // bind mount wayland socket (insecure)
hlog.Verbose("direct wayland access, PROCEED WITH CAUTION")
share.ensureRuntimeDir()
k.container.Bind(socketPath, innerPath, 0)
k.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute)
}
}
if config.Enablements.Unwrap()&system.EX11 != 0 {
if d, ok := k.lookupEnv(display); !ok {
return newWithMessage("DISPLAY is not set")
} else {
socketDir := container.AbsFHSTmp.Append(".X11-unix")
// the socket file at `/tmp/.X11-unix/X%d` is typically owned by the priv user
// and not accessible by the target user
var socketPath *container.Absolute
if len(d) > 1 && d[0] == ':' { // `:%d`
if n, err := strconv.Atoi(d[1:]); err == nil && n >= 0 {
socketPath = socketDir.Append("X" + strconv.Itoa(n))
}
} else if len(d) > 5 && strings.HasPrefix(d, "unix:") { // `unix:%s`
if a, err := container.NewAbs(d[5:]); err == nil {
socketPath = a
}
}
if socketPath != nil {
if _, err := k.stat(socketPath.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err}
}
} else {
k.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute)
if !config.Container.HostAbstract {
d = "unix:" + socketPath.String()
}
}
}
k.sys.ChangeHosts("#" + k.user.uid.String())
k.env[display] = d
k.container.Bind(socketDir, socketDir, 0)
}
}
if config.Enablements.Unwrap()&system.EPulse != 0 {
// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
pulseRuntimeDir := share.sc.RuntimePath.Append("pulse")
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := pulseRuntimeDir.Append("native")
if _, err := k.stat(pulseRuntimeDir.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err}
}
return newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
}
if s, err := k.stat(pulseSocket.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err}
}
return newWithMessage(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
} else {
if m := s.Mode(); m&0o006 != 0o006 {
return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m))
}
}
// hard link pulse socket into target-executable share
innerPulseRuntimeDir := share.runtime().Append("pulse")
innerPulseSocket := innerRuntimeDir.Append("pulse", "native")
k.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String())
k.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
k.env[pulseServer] = "unix:" + innerPulseSocket.String()
// publish current user's pulse cookie for target user
var paCookiePath *container.Absolute
{
const paLocateStep = "locate PulseAudio cookie"
// from environment
if p, ok := k.lookupEnv(pulseCookie); ok {
if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err}
} else {
// this takes precedence, do not verify whether the file is accessible
paCookiePath = a
goto out
}
}
// $HOME/.pulse-cookie
if p, ok := k.lookupEnv(home); ok {
if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err}
} else {
paCookiePath = a.Append(".pulse-cookie")
}
if s, err := k.stat(paCookiePath.String()); err != nil {
paCookiePath = nil
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
}
// fallthrough
} else if s.IsDir() {
paCookiePath = nil
} else {
goto out
}
}
// $XDG_CONFIG_HOME/pulse/cookie
if p, ok := k.lookupEnv(xdgConfigHome); ok {
if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err}
} else {
paCookiePath = a.Append("pulse", "cookie")
}
if s, err := k.stat(paCookiePath.String()); err != nil {
paCookiePath = nil
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
}
// fallthrough
} else if s.IsDir() {
paCookiePath = nil
} else {
goto out
}
}
out:
}
if paCookiePath != nil {
innerDst := hst.AbsTmp.Append("/pulse-cookie")
k.env[pulseCookie] = innerDst.String()
var payload *[]byte
k.container.PlaceP(innerDst, &payload)
k.sys.CopyFile(payload, paCookiePath.String(), 256, 256)
} else {
hlog.Verbose("cannot locate PulseAudio cookie (tried " +
"$PULSE_COOKIE, " +
"$XDG_CONFIG_HOME/pulse/cookie, " +
"$HOME/.pulse-cookie)")
}
}
if config.Enablements.Unwrap()&system.EDBus != 0 {
// ensure dbus session bus defaults
if config.SessionBus == nil {
config.SessionBus = dbus.NewConfig(config.ID, true, true)
}
// downstream socket paths
sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket")
// configure dbus proxy
if f, err := k.sys.ProxyDBus(
config.SessionBus, config.SystemBus,
sessionPath.String(), systemPath.String(),
); err != nil {
return err
} else {
k.dbusMsg = f
}
sys := system.New(k.ctx, msg, s.uid.unwrap())
stateSys := outcomeStateSys{sys: sys, outcomeState: &s}
for _, op := range s.Shim.Ops {
if err := op.toSystem(&stateSys, config); err != nil {
return err
// share proxy sockets
sessionInner := innerRuntimeDir.Append("bus")
k.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String()
k.container.Bind(sessionPath, sessionInner, 0)
k.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write)
if config.SystemBus != nil {
systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket")
k.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String()
k.container.Bind(systemPath, systemInner, 0)
k.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write)
}
}
k.sys = sys
k.supp = supp
k.state = &s
// mount root read-only as the final setup Op
k.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY)
// append ExtraPerms last
for _, p := range config.ExtraPerms {
if p == nil || p.Path == nil {
continue
}
if p.Ensure {
k.sys.Ensure(p.Path.String(), 0700)
}
perms := make(acl.Perms, 0, 3)
if p.Read {
perms = append(perms, acl.Read)
}
if p.Write {
perms = append(perms, acl.Write)
}
if p.Execute {
perms = append(perms, acl.Execute)
}
k.sys.UpdatePermType(system.User, p.Path.String(), perms...)
}
// flatten and sort env for deterministic behaviour
k.container.Env = make([]string, 0, len(k.env))
for key, value := range k.env {
if strings.IndexByte(key, '=') != -1 {
return &hst.AppError{Step: "flatten environment", Err: syscall.EINVAL,
Msg: fmt.Sprintf("invalid environment variable %s", key)}
}
k.container.Env = append(k.container.Env, key+"="+value)
}
slices.Sort(k.container.Env)
if hlog.Load() {
hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
k.user.uid, k.user.username, config.Groups, k.container.Args, len(*k.container.Ops))
}
return nil
}

View File

@ -10,8 +10,8 @@ import (
"sync"
"hakurei.app/container"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
// Hsu caches responses from cmd/hsu.
@ -40,13 +40,13 @@ func (h *Hsu) ID() (int, error) {
h.ensureDispatcher()
h.idOnce.Do(func() {
h.id = -1
hsuPath := h.k.mustHsuPath().String()
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 = fhs.Root
cmd.Dir = container.FHSRoot
var (
p []byte
exitError *exec.ExitError
@ -71,10 +71,7 @@ func (h *Hsu) ID() (int, error) {
}
// MustID calls [Hsu.ID] and terminates on error.
func (h *Hsu) MustID() int { return h.MustIDMsg(nil) }
// MustIDMsg implements MustID with a custom [container.Msg].
func (h *Hsu) MustIDMsg(msg container.Msg) int {
func (h *Hsu) MustID() int {
id, err := h.ID()
if err == nil {
return id
@ -82,9 +79,7 @@ func (h *Hsu) MustIDMsg(msg container.Msg) int {
const fallback = "cannot retrieve user id from setuid wrapper:"
if errors.Is(err, ErrHsuAccess) {
if msg != nil {
msg.Verbose("*"+fallback, err)
}
hlog.Verbose("*"+fallback, err)
os.Exit(1)
return -0xdeadbeef
} else if m, ok := container.GetErrorMessage(err); ok {

View File

@ -1,248 +0,0 @@
package app
import (
"os"
"strconv"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/system"
"hakurei.app/system/acl"
)
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
// stringPair stores a value and its string representation.
type stringPair[T comparable] struct {
v T
s string
}
func (s *stringPair[T]) unwrap() T { return s.v }
func (s *stringPair[T]) String() string { return s.s }
// outcomeState is copied to the shim process and available while applying outcomeOp.
// This is transmitted from the priv side to the shim, so exported fields should be kept to a minimum.
type outcomeState struct {
// Params only used by the shim process. Populated by populateEarly.
Shim *shimParams
// Generated and accounted for by the caller.
ID *state.ID
// Copied from ID.
id *stringPair[state.ID]
// Copied from the [hst.Config] field of the same name.
Identity int
// Copied from Identity.
identity *stringPair[int]
// Returned by [Hsu.MustIDMsg].
UserID int
// Target init namespace uid resolved from UserID and identity.
uid *stringPair[int]
// Included as part of [hst.Config], transmitted as-is unless permissive defaults.
Container *hst.ContainerConfig
// Mapped credentials within container user namespace.
Mapuid, Mapgid int
// Copied from their respective exported values.
mapuid, mapgid *stringPair[int]
// Copied from [EnvPaths] per-process.
sc hst.Paths
*EnvPaths
// Matched paths to cover. Populated by spFilesystemOp.
HidePaths []*check.Absolute
// Copied via populateLocal.
k syscallDispatcher
// Copied via populateLocal.
msg container.Msg
}
// valid checks outcomeState to be safe for use with outcomeOp.
func (s *outcomeState) valid() bool {
return s != nil &&
s.Shim.valid() &&
s.ID != nil &&
s.Container != nil &&
s.EnvPaths != nil
}
// populateEarly populates exported fields via syscallDispatcher.
// This must only be called from the priv side.
func (s *outcomeState) populateEarly(k syscallDispatcher, msg container.Msg, config *hst.Config) {
s.Shim = &shimParams{PrivPID: os.Getpid(), Verbose: msg.IsVerbose(), Ops: fromConfig(config)}
// enforce bounds and default early
if s.Container.WaitDelay <= 0 {
s.Shim.WaitDelay = hst.WaitDelayDefault
} else if s.Container.WaitDelay > hst.WaitDelayMax {
s.Shim.WaitDelay = hst.WaitDelayMax
} else {
s.Shim.WaitDelay = s.Container.WaitDelay
}
if s.Container.MapRealUID {
s.Mapuid, s.Mapgid = k.getuid(), k.getgid()
} else {
s.Mapuid, s.Mapgid = k.overflowUid(msg), k.overflowGid(msg)
}
return
}
// populateLocal populates unexported fields from transmitted exported fields.
// These fields are cheaper to recompute per-process.
func (s *outcomeState) populateLocal(k syscallDispatcher, msg container.Msg) error {
if !s.valid() || k == nil || msg == nil {
return newWithMessage("impossible outcome state reached")
}
if s.k != nil || s.msg != nil {
panic("attempting to call populateLocal twice")
}
s.k = k
s.msg = msg
s.id = &stringPair[state.ID]{*s.ID, s.ID.String()}
s.Copy(&s.sc, s.UserID)
msg.Verbosef("process share directory at %q, runtime directory at %q", s.sc.SharePath, s.sc.RunDirPath)
s.identity = newInt(s.Identity)
s.mapuid, s.mapgid = newInt(s.Mapuid), newInt(s.Mapgid)
s.uid = newInt(HsuUid(s.UserID, s.identity.unwrap()))
return nil
}
// instancePath returns a path formatted for outcomeStateSys.instance.
// This method must only be called from outcomeOp.toContainer if
// outcomeOp.toSystem has already called outcomeStateSys.instance.
func (s *outcomeState) instancePath() *check.Absolute { return s.sc.SharePath.Append(s.id.String()) }
// runtimePath returns a path formatted for outcomeStateSys.runtime.
// This method must only be called from outcomeOp.toContainer if
// outcomeOp.toSystem has already called outcomeStateSys.runtime.
func (s *outcomeState) runtimePath() *check.Absolute { return s.sc.RunDirPath.Append(s.id.String()) }
// outcomeStateSys wraps outcomeState and [system.I]. Used on the priv side only.
// Implementations of outcomeOp must not access fields other than sys unless explicitly stated.
type outcomeStateSys struct {
// Whether XDG_RUNTIME_DIR is used post hsu.
useRuntimeDir bool
// Process-specific directory in TMPDIR, nil if unused.
sharePath *check.Absolute
// Process-specific directory in XDG_RUNTIME_DIR, nil if unused.
runtimeSharePath *check.Absolute
sys *system.I
*outcomeState
}
// ensureRuntimeDir must be called if access to paths within XDG_RUNTIME_DIR is required.
func (state *outcomeStateSys) ensureRuntimeDir() {
if state.useRuntimeDir {
return
}
state.useRuntimeDir = true
state.sys.Ensure(state.sc.RunDirPath, 0700)
state.sys.UpdatePermType(system.User, state.sc.RunDirPath, acl.Execute)
state.sys.Ensure(state.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
state.sys.UpdatePermType(system.User, state.sc.RuntimePath, acl.Execute)
}
// instance returns the pathname to a process-specific directory within TMPDIR.
// This directory must only hold entries bound to [system.Process].
func (state *outcomeStateSys) instance() *check.Absolute {
if state.sharePath != nil {
return state.sharePath
}
state.sharePath = state.instancePath()
state.sys.Ephemeral(system.Process, state.sharePath, 0711)
return state.sharePath
}
// runtime returns the pathname to a process-specific directory within XDG_RUNTIME_DIR.
// This directory must only hold entries bound to [system.Process].
func (state *outcomeStateSys) runtime() *check.Absolute {
if state.runtimeSharePath != nil {
return state.runtimeSharePath
}
state.ensureRuntimeDir()
state.runtimeSharePath = state.runtimePath()
state.sys.Ephemeral(system.Process, state.runtimeSharePath, 0700)
state.sys.UpdatePerm(state.runtimeSharePath, acl.Execute)
return state.runtimeSharePath
}
// outcomeStateParams wraps outcomeState and [container.Params]. Used on the shim side only.
type outcomeStateParams struct {
// Overrides the embedded [container.Params] in [container.Container]. The Env field must not be used.
params *container.Params
// Collapsed into the Env slice in [container.Params] by the final outcomeOp.
env map[string]string
// Filesystems with the optional root sliced off if present. Populated by spParamsOp.
// Safe for use by spFilesystemOp.
filesystem []hst.FilesystemConfigJSON
// Inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` via mapped uid.
// Populated by spRuntimeOp.
runtimeDir *check.Absolute
as hst.ApplyState
*outcomeState
}
// An outcomeOp inflicts an outcome on [system.I] and contains enough information to
// inflict it on [container.Params] in a separate process.
// An implementation of outcomeOp must store cross-process states in exported fields only.
type outcomeOp interface {
// toSystem inflicts the current outcome on [system.I] in the priv side process.
toSystem(state *outcomeStateSys, config *hst.Config) error
// toContainer inflicts the current outcome on [container.Params] in the shim process.
// The implementation must not write to the Env field of [container.Params] as it will be overwritten
// by flattened env map.
toContainer(state *outcomeStateParams) error
}
// fromConfig returns a corresponding slice of outcomeOp for [hst.Config].
// This function assumes the caller has already called the Validate method on [hst.Config]
// and checked that it returns nil.
func fromConfig(config *hst.Config) (ops []outcomeOp) {
ops = []outcomeOp{
// must run first
&spParamsOp{},
// TODO(ophestra): move this late for #8 and #9
spFilesystemOp{},
spRuntimeOp{},
spTmpdirOp{},
spAccountOp{},
}
et := config.Enablements.Unwrap()
if et&hst.EWayland != 0 {
ops = append(ops, &spWaylandOp{})
}
if et&hst.EX11 != 0 {
ops = append(ops, &spX11Op{})
}
if et&hst.EPulse != 0 {
ops = append(ops, &spPulseOp{})
}
if et&hst.EDBus != 0 {
ops = append(ops, &spDBusOp{})
}
ops = append(ops, spFinal{})
return
}

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