diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index 8374975..d1d6730 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -115,9 +115,15 @@ func buildCommand(out io.Writer) command.Command { config.Identity = aid config.Groups = groups - config.Data = homeDir config.Username = userName + if a, err := container.NewAbs(homeDir); err != nil { + log.Fatal(err.Error()) + return err + } else { + config.Data = a + } + if wayland { config.Enablements |= system.EWayland } @@ -213,7 +219,7 @@ func buildCommand(out io.Writer) command.Command { var psFlagShort bool c.NewCommand("ps", "List active instances", func(args []string) error { - printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath), psFlagShort, flagJSON) + printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), psFlagShort, flagJSON) return errSuccess }).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id") diff --git a/cmd/hakurei/parse.go b/cmd/hakurei/parse.go index bfbe144..56ff83e 100644 --- a/cmd/hakurei/parse.go +++ b/cmd/hakurei/parse.go @@ -87,7 +87,7 @@ func tryShort(name string) (config *hst.Config, entry *state.State) { if likePrefix && len(name) >= 8 { hlog.Verbose("argument looks like prefix") - s := state.NewMulti(std.Paths().RunDirPath) + s := state.NewMulti(std.Paths().RunDirPath.String()) if entries, err := state.Join(s); err != nil { log.Printf("cannot join store: %v", err) // drop to fetch from file diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index 1169d10..af053d0 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -12,6 +12,7 @@ import ( "text/tabwriter" "time" + "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal/app/state" "hakurei.app/internal/hlog" @@ -77,13 +78,13 @@ func printShowInstance( if len(config.Groups) > 0 { t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", ")) } - if config.Data != "" { + if config.Data != nil { t.Printf(" Data:\t%s\n", config.Data) } if config.Container != nil { - container := config.Container - if container.Hostname != "" { - t.Printf(" Hostname:\t%s\n", container.Hostname) + params := config.Container + if params.Hostname != "" { + t.Printf(" Hostname:\t%s\n", params.Hostname) } flags := make([]string, 0, 7) writeFlag := func(name string, value bool) { @@ -91,30 +92,32 @@ func printShowInstance( flags = append(flags, name) } } - writeFlag("userns", container.Userns) - writeFlag("devel", container.Devel) - writeFlag("net", container.Net) - writeFlag("device", container.Device) - writeFlag("tty", container.Tty) - writeFlag("mapuid", container.MapRealUID) + writeFlag("userns", params.Userns) + writeFlag("devel", params.Devel) + writeFlag("net", params.Net) + writeFlag("device", params.Device) + writeFlag("tty", params.Tty) + writeFlag("mapuid", params.MapRealUID) writeFlag("directwl", config.DirectWayland) - writeFlag("autoetc", container.AutoEtc) + writeFlag("autoetc", params.AutoEtc) if len(flags) == 0 { flags = append(flags, "none") } t.Printf(" Flags:\t%s\n", strings.Join(flags, " ")) - if container.AutoRoot != "" { - t.Printf(" Root:\t%s (%d)\n", container.AutoRoot, container.RootFlags) + if params.AutoRoot != nil { + t.Printf(" Root:\t%s (%d)\n", params.AutoRoot, params.RootFlags) } - etc := container.Etc - if etc == "" { - etc = "/etc" + etc := params.Etc + if etc == nil { + etc = container.AbsFHSEtc } t.Printf(" Etc:\t%s\n", etc) - t.Printf(" Path:\t%s\n", config.Path) + if config.Path != nil { + t.Printf(" Path:\t%s\n", config.Path) + } } if len(config.Args) > 0 { t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " ")) @@ -125,12 +128,19 @@ func printShowInstance( if config.Container != nil && len(config.Container.Filesystem) > 0 { t.Printf("Filesystem\n") for _, f := range config.Container.Filesystem { - if f == nil { + g := 4 + if f.Src == nil { + t.Println(" ") continue + } else { + g += len(f.Src.String()) + } + if f.Dst != nil { + g += len(f.Dst.String()) } expr := new(strings.Builder) - expr.Grow(3 + len(f.Src) + 1 + len(f.Dst)) + expr.Grow(g) if f.Device { expr.WriteString(" d") @@ -144,9 +154,14 @@ func printShowInstance( } else { expr.WriteString("+") } - expr.WriteString(f.Src) - if f.Dst != "" { - expr.WriteString(":" + f.Dst) + src := f.Src.String() + if src != container.Nonexistent { + expr.WriteString(src) + } else { + expr.WriteString("tmpfs") + } + if f.Dst != nil { + expr.WriteString(":" + f.Dst.String()) } t.Printf("%s\n", expr.String()) } diff --git a/cmd/hakurei/print_test.go b/cmd/hakurei/print_test.go index 4a79ba6..6f4a675 100644 --- a/cmd/hakurei/print_test.go +++ b/cmd/hakurei/print_test.go @@ -83,18 +83,17 @@ App Identity: 0 Enablements: (no enablements) Flags: none - Etc: /etc - Path: + Etc: /etc/ `}, - {"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]*hst.FilesystemConfig, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App + {"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfig, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App Identity: 0 Enablements: (no enablements) Flags: none - Etc: /etc - Path: + Etc: /etc/ Filesystem + Extra ACL @@ -277,7 +276,7 @@ App "filesystem": [ { "dst": "/tmp/", - "src": "tmpfs", + "src": "/proc/nonexistent", "write": true }, { @@ -304,10 +303,10 @@ App } ], "symlink": [ - [ - "/run/user/65534", - "/run/user/150" - ] + { + "target": "/run/user/65534", + "linkname": "/run/user/150" + } ], "auto_root": "/var/lib/hakurei/base/org.debian", "root_flags": 2, @@ -409,7 +408,7 @@ App "filesystem": [ { "dst": "/tmp/", - "src": "tmpfs", + "src": "/proc/nonexistent", "write": true }, { @@ -436,10 +435,10 @@ App } ], "symlink": [ - [ - "/run/user/65534", - "/run/user/150" - ] + { + "target": "/run/user/65534", + "linkname": "/run/user/150" + } ], "auto_root": "/var/lib/hakurei/base/org.debian", "root_flags": 2, @@ -595,7 +594,7 @@ func Test_printPs(t *testing.T) { "filesystem": [ { "dst": "/tmp/", - "src": "tmpfs", + "src": "/proc/nonexistent", "write": true }, { @@ -622,10 +621,10 @@ func Test_printPs(t *testing.T) { } ], "symlink": [ - [ - "/run/user/65534", - "/run/user/150" - ] + { + "target": "/run/user/65534", + "linkname": "/run/user/150" + } ], "auto_root": "/var/lib/hakurei/base/org.debian", "root_flags": 2, diff --git a/cmd/hpkg/app.go b/cmd/hpkg/app.go index fb784d7..705b393 100644 --- a/cmd/hpkg/app.go +++ b/cmd/hpkg/app.go @@ -4,7 +4,6 @@ import ( "encoding/json" "log" "os" - "path" "hakurei.app/container" "hakurei.app/container/seccomp" @@ -56,18 +55,18 @@ type appInfo struct { // store path to nixGL source NixGL string `json:"nix_gl,omitempty"` // store path to activate-and-exec script - Launcher string `json:"launcher"` + Launcher *container.Absolute `json:"launcher"` // store path to /run/current-system - CurrentSystem string `json:"current_system"` + CurrentSystem *container.Absolute `json:"current_system"` // store path to home-manager activation package ActivationPackage string `json:"activation_package"` } -func (app *appInfo) toFst(pathSet *appPathSet, 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: argv[0], + Path: pathname, Args: argv, Enablements: app.Enablements, @@ -77,9 +76,9 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool DirectWayland: app.DirectWayland, Username: "hakurei", - Shell: shellPath, + Shell: pathShell, Data: pathSet.homeDir, - Dir: path.Join("/data/data", app.ID), + Dir: pathDataData.Append(app.ID), Identity: app.Identity, Groups: app.Groups, @@ -92,22 +91,22 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool Device: app.Device, Tty: app.Tty || flagDropShell, MapRealUID: app.MapRealUID, - Filesystem: []*hst.FilesystemConfig{ - {Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true}, - {Src: pathSet.metaPath, Dst: path.Join(hst.Tmp, "app"), Must: true}, - {Src: container.FHSEtc + "resolv.conf"}, - {Src: container.FHSSys + "block"}, - {Src: container.FHSSys + "bus"}, - {Src: container.FHSSys + "class"}, - {Src: container.FHSSys + "dev"}, - {Src: container.FHSSys + "devices"}, + Filesystem: []hst.FilesystemConfig{ + {Src: pathSet.nixPath.Append("store"), Dst: pathNixStore, Must: true}, + {Src: pathSet.metaPath, Dst: hst.AbsTmp.Append("app"), Must: true}, + {Src: container.AbsFHSEtc.Append("resolv.conf")}, + {Src: container.AbsFHSSys.Append("block")}, + {Src: container.AbsFHSSys.Append("bus")}, + {Src: container.AbsFHSSys.Append("class")}, + {Src: container.AbsFHSSys.Append("dev")}, + {Src: container.AbsFHSSys.Append("devices")}, }, - Link: [][2]string{ - {app.CurrentSystem, container.FHSRun + "current-system"}, - {container.FHSRun + "current-system/sw/bin", "/bin"}, - {container.FHSRun + "current-system/sw/bin", container.FHSUsrBin}, + Link: []hst.LinkConfig{ + {pathCurrentSystem, app.CurrentSystem.String()}, + {pathBin, pathSwBin.String()}, + {container.AbsFHSUsrBin, pathSwBin.String()}, }, - Etc: path.Join(pathSet.cacheDir, "etc"), + Etc: pathSet.cacheDir.Append("etc"), AutoEtc: true, }, ExtraPerms: []*hst.ExtraPermConfig{ @@ -141,6 +140,14 @@ func loadAppInfo(name string, beforeFail func()) *appInfo { beforeFail() log.Fatal("application identifier must not be empty") } + if bundle.Launcher == nil { + beforeFail() + log.Fatal("launcher must not be empty") + } + if bundle.CurrentSystem == nil { + beforeFail() + log.Fatal("current-system must not be empty") + } return bundle } diff --git a/cmd/hpkg/main.go b/cmd/hpkg/main.go index 728ee14..b8d6304 100644 --- a/cmd/hpkg/main.go +++ b/cmd/hpkg/main.go @@ -17,15 +17,13 @@ import ( "hakurei.app/internal/hlog" ) -const shellPath = "/run/current-system/sw/bin/bash" - var ( errSuccess = errors.New("success") ) func init() { hlog.Prepare("hpkg") - if err := os.Setenv("SHELL", shellPath); err != nil { + if err := os.Setenv("SHELL", pathShell.String()); err != nil { log.Fatalf("cannot set $SHELL: %v", err) } } @@ -82,31 +80,32 @@ func main() { Extract package and set up for cleanup. */ - var workDir string + var workDir *container.Absolute if p, err := os.MkdirTemp("", "hpkg.*"); err != nil { log.Printf("cannot create temporary directory: %v", err) return err - } else { - workDir = p + } 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(chmod, "-R", "+w", workDir) - mustRun(rm, "-rf", workDir) + mustRun(chmod, "-R", "+w", workDir.String()) + mustRun(rm, "-rf", workDir.String()) } beforeRunFail.Store(&cleanup) - mustRun(tar, "-C", workDir, "-xf", pkgPath) + mustRun(tar, "-C", workDir.String(), "-xf", pkgPath) /* Parse bundle and app metadata, do pre-install checks. */ - bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup) + bundle := loadAppInfo(path.Join(workDir.String(), "bundle.json"), cleanup) pathSet := pathSetByApp(bundle.ID) a := bundle - if s, err := os.Stat(pathSet.metaPath); err != nil { + if s, err := os.Stat(pathSet.metaPath.String()); err != nil { if !os.IsNotExist(err) { cleanup() log.Printf("cannot access %q: %v", pathSet.metaPath, err) @@ -118,7 +117,7 @@ func main() { log.Printf("metadata path %q is not a file", pathSet.metaPath) return syscall.EBADMSG } else { - a = loadAppInfo(pathSet.metaPath, cleanup) + a = loadAppInfo(pathSet.metaPath.String(), cleanup) if a.ID != bundle.ID { cleanup() log.Printf("app %q claims to have identifier %q", @@ -209,7 +208,7 @@ func main() { */ // serialise metadata to ensure consistency - if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil { + if f, err := os.OpenFile(pathSet.metaPath.String()+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil { cleanup() log.Printf("cannot create metadata file: %v", err) return err @@ -222,7 +221,7 @@ func main() { // not fatal } - if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil { + if err := os.Rename(pathSet.metaPath.String()+"~", pathSet.metaPath.String()); err != nil { cleanup() log.Printf("cannot rename metadata file: %v", err) return err @@ -251,7 +250,7 @@ func main() { id := args[0] pathSet := pathSetByApp(id) - a := loadAppInfo(pathSet.metaPath, func() {}) + a := loadAppInfo(pathSet.metaPath.String(), func() {}) if a.ID != id { log.Printf("app %q claims to have identifier %q", id, a.ID) return syscall.EBADE @@ -275,13 +274,13 @@ func main() { "--override-input nixpkgs path:/etc/nixpkgs " + "path:" + a.NixGL + "#nixVulkanNvidia", }, true, func(config *hst.Config) *hst.Config { - config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{ - {Src: container.FHSEtc + "resolv.conf"}, - {Src: container.FHSSys + "block"}, - {Src: container.FHSSys + "bus"}, - {Src: container.FHSSys + "class"}, - {Src: container.FHSSys + "dev"}, - {Src: container.FHSSys + "devices"}, + config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfig{ + {Src: container.AbsFHSEtc.Append("resolv.conf")}, + {Src: container.AbsFHSSys.Append("block")}, + {Src: container.AbsFHSSys.Append("bus")}, + {Src: container.AbsFHSSys.Append("class")}, + {Src: container.AbsFHSSys.Append("dev")}, + {Src: container.AbsFHSSys.Append("devices")}, }...) appendGPUFilesystem(config) return config @@ -292,15 +291,16 @@ func main() { Create app configuration. */ + pathname := a.Launcher argv := make([]string, 1, len(args)) - if !flagDropShell { - argv[0] = a.Launcher + if flagDropShell { + pathname = pathShell + argv[0] = bash } else { - argv[0] = shellPath + argv[0] = a.Launcher.String() } argv = append(argv, args[1:]...) - - config := a.toFst(pathSet, argv, flagDropShell) + config := a.toHst(pathSet, pathname, argv, flagDropShell) /* Expose GPU devices. @@ -308,7 +308,7 @@ func main() { if a.GPU { config.Container.Filesystem = append(config.Container.Filesystem, - &hst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(hst.Tmp, "nixGL")}) + hst.FilesystemConfig{Src: pathSet.nixPath.Append(".nixGL"), Dst: hst.AbsTmp.Append("nixGL")}) appendGPUFilesystem(config) } diff --git a/cmd/hpkg/paths.go b/cmd/hpkg/paths.go index b7cc926..095c24e 100644 --- a/cmd/hpkg/paths.go +++ b/cmd/hpkg/paths.go @@ -4,7 +4,6 @@ import ( "log" "os" "os/exec" - "path" "strconv" "sync/atomic" @@ -13,19 +12,34 @@ import ( "hakurei.app/internal/hlog" ) +const bash = "bash" + var ( - dataHome string + dataHome *container.Absolute ) func init() { // dataHome - if p, ok := os.LookupEnv("HAKUREI_DATA_HOME"); ok { - dataHome = p + if a, err := container.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil { + dataHome = a } else { - dataHome = container.FHSVarLib + "hakurei/" + strconv.Itoa(os.Getuid()) + dataHome = container.AbsFHSVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid())) } } +var ( + pathBin = container.AbsFHSRoot.Append("bin") + + pathNix = container.MustAbs("/nix/") + pathNixStore = pathNix.Append("store/") + pathCurrentSystem = container.AbsFHSRun.Append("current-system") + pathSwBin = pathCurrentSystem.Append("sw/bin/") + pathShell = pathSwBin.Append(bash) + + pathData = container.MustAbs("/data") + pathDataData = pathData.Append("data") +) + func lookPath(file string) string { if p, err := exec.LookPath(file); err != nil { log.Fatalf("%s: command not found", file) @@ -51,52 +65,52 @@ func mustRun(name string, arg ...string) { type appPathSet struct { // ${dataHome}/${id} - baseDir string + baseDir *container.Absolute // ${baseDir}/app - metaPath string + metaPath *container.Absolute // ${baseDir}/files - homeDir string + homeDir *container.Absolute // ${baseDir}/cache - cacheDir string + cacheDir *container.Absolute // ${baseDir}/cache/nix - nixPath string + nixPath *container.Absolute } func pathSetByApp(id string) *appPathSet { pathSet := new(appPathSet) - pathSet.baseDir = path.Join(dataHome, id) - pathSet.metaPath = path.Join(pathSet.baseDir, "app") - pathSet.homeDir = path.Join(pathSet.baseDir, "files") - pathSet.cacheDir = path.Join(pathSet.baseDir, "cache") - pathSet.nixPath = path.Join(pathSet.cacheDir, "nix") + pathSet.baseDir = dataHome.Append(id) + pathSet.metaPath = pathSet.baseDir.Append("app") + pathSet.homeDir = pathSet.baseDir.Append("files") + pathSet.cacheDir = pathSet.baseDir.Append("cache") + pathSet.nixPath = pathSet.cacheDir.Append("nix") return pathSet } func appendGPUFilesystem(config *hst.Config) { - config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{ + config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfig{ // flatpak commit 763a686d874dd668f0236f911de00b80766ffe79 - {Src: "/dev/dri", Device: true}, + {Src: container.AbsFHSDev.Append("dri"), Device: true}, // mali - {Src: "/dev/mali", Device: true}, - {Src: "/dev/mali0", Device: true}, - {Src: "/dev/umplock", Device: true}, + {Src: container.AbsFHSDev.Append("mali"), Device: true}, + {Src: container.AbsFHSDev.Append("mali0"), Device: true}, + {Src: container.AbsFHSDev.Append("umplock"), Device: true}, // nvidia - {Src: "/dev/nvidiactl", Device: true}, - {Src: "/dev/nvidia-modeset", Device: true}, + {Src: container.AbsFHSDev.Append("nvidiactl"), Device: true}, + {Src: container.AbsFHSDev.Append("nvidia-modeset"), Device: true}, // nvidia OpenCL/CUDA - {Src: "/dev/nvidia-uvm", Device: true}, - {Src: "/dev/nvidia-uvm-tools", Device: true}, + {Src: container.AbsFHSDev.Append("nvidia-uvm"), Device: true}, + {Src: container.AbsFHSDev.Append("nvidia-uvm-tools"), Device: true}, // flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff - {Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true}, - {Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true}, - {Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true}, - {Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true}, - {Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true}, - {Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true}, - {Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true}, - {Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true}, - {Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true}, - {Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true}, + {Src: container.AbsFHSDev.Append("nvidia0"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia1"), Device: true}, + {Src: container.AbsFHSDev.Append("nvidia2"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia3"), Device: true}, + {Src: container.AbsFHSDev.Append("nvidia4"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia5"), Device: true}, + {Src: container.AbsFHSDev.Append("nvidia6"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia7"), Device: true}, + {Src: container.AbsFHSDev.Append("nvidia8"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia9"), Device: true}, + {Src: container.AbsFHSDev.Append("nvidia10"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia11"), Device: true}, + {Src: container.AbsFHSDev.Append("nvidia12"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia13"), Device: true}, + {Src: container.AbsFHSDev.Append("nvidia14"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia15"), Device: true}, + {Src: container.AbsFHSDev.Append("nvidia16"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia17"), Device: true}, + {Src: container.AbsFHSDev.Append("nvidia18"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia19"), Device: true}, }...) } diff --git a/cmd/hpkg/with.go b/cmd/hpkg/with.go index 444b4e7..72ca245 100644 --- a/cmd/hpkg/with.go +++ b/cmd/hpkg/with.go @@ -2,7 +2,6 @@ package main import ( "context" - "path" "strings" "hakurei.app/container" @@ -19,8 +18,8 @@ func withNixDaemon( mustRunAppDropShell(ctx, updateConfig(&hst.Config{ ID: app.ID, - Path: shellPath, - Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " + + Path: pathShell, + Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " + // start nix-daemon "nix-daemon --store / & " + // wait for socket to appear @@ -33,9 +32,9 @@ func withNixDaemon( }, Username: "hakurei", - Shell: shellPath, + Shell: pathShell, Data: pathSet.homeDir, - Dir: path.Join("/data/data", app.ID), + Dir: pathDataData.Append(app.ID), ExtraPerms: []*hst.ExtraPermConfig{ {Path: dataHome, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, @@ -49,15 +48,15 @@ func withNixDaemon( Net: net, SeccompFlags: seccomp.AllowMultiarch, Tty: dropShell, - Filesystem: []*hst.FilesystemConfig{ - {Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true}, + Filesystem: []hst.FilesystemConfig{ + {Src: pathSet.nixPath, Dst: pathNix, Write: true, Must: true}, }, - Link: [][2]string{ - {app.CurrentSystem, container.FHSRun + "current-system"}, - {container.FHSRun + "current-system/sw/bin", "/bin"}, - {container.FHSRun + "current-system/sw/bin", container.FHSUsrBin}, + Link: []hst.LinkConfig{ + {pathCurrentSystem, app.CurrentSystem.String()}, + {pathBin, pathSwBin.String()}, + {container.AbsFHSUsrBin, pathSwBin.String()}, }, - Etc: path.Join(pathSet.cacheDir, "etc"), + Etc: pathSet.cacheDir.Append("etc"), AutoEtc: true, }, }), dropShell, beforeFail) @@ -65,18 +64,18 @@ func withNixDaemon( func withCacheDir( ctx context.Context, - action string, command []string, workDir string, + action string, command []string, workDir *container.Absolute, app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) { mustRunAppDropShell(ctx, &hst.Config{ ID: app.ID, - Path: shellPath, - Args: []string{shellPath, "-lc", strings.Join(command, " && ")}, + Path: pathShell, + Args: []string{bash, "-lc", strings.Join(command, " && ")}, Username: "nixos", - Shell: shellPath, + Shell: pathShell, Data: pathSet.cacheDir, // this also ensures cacheDir via shim - Dir: path.Join("/data/data", app.ID, "cache"), + Dir: pathDataData.Append(app.ID, "cache"), ExtraPerms: []*hst.ExtraPermConfig{ {Path: dataHome, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, @@ -89,16 +88,16 @@ func withCacheDir( Hostname: formatHostname(app.Name) + "-" + action, SeccompFlags: seccomp.AllowMultiarch, Tty: dropShell, - Filesystem: []*hst.FilesystemConfig{ - {Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true}, - {Src: workDir, Dst: path.Join(hst.Tmp, "bundle"), Must: true}, + Filesystem: []hst.FilesystemConfig{ + {Src: workDir.Append("nix"), Dst: pathNix, Must: true}, + {Src: workDir, Dst: hst.AbsTmp.Append("bundle"), Must: true}, }, - Link: [][2]string{ - {app.CurrentSystem, container.FHSRun + "current-system"}, - {container.FHSRun + "current-system/sw/bin", "/bin"}, - {container.FHSRun + "current-system/sw/bin", container.FHSUsrBin}, + Link: []hst.LinkConfig{ + {pathCurrentSystem, app.CurrentSystem.String()}, + {pathBin, pathSwBin.String()}, + {container.AbsFHSUsrBin, pathSwBin.String()}, }, - Etc: path.Join(workDir, container.FHSEtc), + Etc: workDir.Append(container.FHSEtc), AutoEtc: true, }, }, dropShell, beforeFail) @@ -106,7 +105,7 @@ func withCacheDir( func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) { if dropShell { - config.Args = []string{shellPath, "-l"} + config.Args = []string{bash, "-l"} mustRunApp(ctx, config, beforeFail) beforeFail() internal.Exit(0) diff --git a/container/autoetc.go b/container/autoetc.go index 3e23c16..5687912 100644 --- a/container/autoetc.go +++ b/container/autoetc.go @@ -10,9 +10,9 @@ 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, prefix string) *Ops { +func (f *Ops) Etc(host *Absolute, prefix string) *Ops { e := &AutoEtcOp{prefix} - f.Mkdir(FHSEtc, 0755) + f.Mkdir(AbsFHSEtc, 0755) f.Bind(host, e.hostPath(), 0) *f = append(*f, e) return f @@ -28,7 +28,7 @@ func (e *AutoEtcOp) apply(*Params) error { if err := os.MkdirAll(target, 0755); err != nil { return wrapErrSelf(err) } - if d, err := os.ReadDir(toSysroot(e.hostPath())); err != nil { + if d, err := os.ReadDir(toSysroot(e.hostPath().String())); err != nil { return wrapErrSelf(err) } else { for _, ent := range d { @@ -54,8 +54,10 @@ func (e *AutoEtcOp) apply(*Params) error { return nil } -func (e *AutoEtcOp) hostPath() string { return FHSEtc + e.hostRel() } -func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix } + +// bypasses abs check, use with caution! +func (e *AutoEtcOp) hostPath() *Absolute { return &Absolute{FHSEtc + e.hostRel()} } +func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix } func (e *AutoEtcOp) Is(op Op) bool { ve, ok := op.(*AutoEtcOp) diff --git a/container/autoroot.go b/container/autoroot.go index 5ed9b6c..70b504e 100644 --- a/container/autoroot.go +++ b/container/autoroot.go @@ -4,21 +4,21 @@ import ( "encoding/gob" "fmt" "os" - "path" - . "syscall" + "syscall" ) 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, prefix string, flags int) *Ops { +func (f *Ops) Root(host *Absolute, prefix string, flags int) *Ops { *f = append(*f, &AutoRootOp{host, prefix, flags, nil}) return f } type AutoRootOp struct { - Host, Prefix string + Host *Absolute + Prefix string // passed through to bindMount Flags int @@ -29,11 +29,11 @@ type AutoRootOp struct { } func (r *AutoRootOp) early(params *Params) error { - if !path.IsAbs(r.Host) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", r.Host)) + if r.Host == nil { + return syscall.EBADE } - if d, err := os.ReadDir(r.Host); err != nil { + if d, err := os.ReadDir(r.Host.String()); err != nil { return wrapErrSelf(err) } else { r.resolved = make([]Op, 0, len(d)) @@ -41,8 +41,8 @@ func (r *AutoRootOp) early(params *Params) error { name := ent.Name() if IsAutoRootBindable(name) { op := &BindMountOp{ - Source: path.Join(r.Host, name), - Target: FHSRoot + name, + Source: r.Host.Append(name), + Target: AbsFHSRoot.Append(name), Flags: r.Flags, } if err = op.early(params); err != nil { diff --git a/container/container.go b/container/container.go index 54cee25..3d6caa6 100644 --- a/container/container.go +++ b/container/container.go @@ -9,7 +9,6 @@ import ( "io" "os" "os/exec" - "path" "strconv" . "syscall" "time" @@ -53,11 +52,11 @@ type ( // Params holds container configuration and is safe to serialise. Params struct { // Working directory in the container. - Dir string + Dir *Absolute // Initial process environment. Env []string - // Absolute path of initial process in the container. Overrides name. - Path string + // Pathname of initial process in the container. + Path *Absolute // Initial process argv. Args []string // Deliver SIGINT to the initial process on context cancellation. @@ -188,14 +187,16 @@ func (p *Container) Serve() error { setup := p.setup p.setup = nil - if !path.IsAbs(p.Path) { + if p.Path == nil { p.cancel() - return msg.WrapErr(EINVAL, - fmt.Sprintf("invalid executable path %q", p.Path)) + return msg.WrapErr(EINVAL, "invalid executable pathname") } + // do not transmit nil + if p.Dir == nil { + p.Dir = AbsFHSRoot + } if p.SeccompRules == nil { - // do not transmit nil p.SeccompRules = make([]seccomp.NativeRule, 0) } @@ -232,11 +233,11 @@ func (p *Container) ProcessState() *os.ProcessState { // New returns the address to a new instance of [Container] that requires further initialisation before use. func New(ctx context.Context) *Container { - return &Container{ctx: ctx, Params: Params{Dir: FHSRoot, Ops: new(Ops)}} + return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}} } // NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields. -func NewCommand(ctx context.Context, pathname, name string, args ...string) *Container { +func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container { z := New(ctx) z.Path = pathname z.Args = append([]string{name}, args...) diff --git a/container/container_test.go b/container/container_test.go index 10c2065..8016aba 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -10,7 +10,6 @@ import ( "os" "os/exec" "os/signal" - "path" "strconv" "strings" "syscall" @@ -77,7 +76,7 @@ var containerTestCases = []struct { {"tmpfs", true, false, false, true, earlyOps(new(container.Ops). - Tmpfs(hst.Tmp, 0, 0755), + Tmpfs(hst.AbsTmp, 0, 0755), ), earlyMnt( ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore), @@ -86,7 +85,7 @@ var containerTestCases = []struct { {"dev", true, true /* go test output is not a tty */, false, false, earlyOps(new(container.Ops). - Dev("/dev", true), + Dev(container.MustAbs("/dev"), true), ), earlyMnt( ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), @@ -103,7 +102,7 @@ var containerTestCases = []struct { {"dev no mqueue", true, true /* go test output is not a tty */, false, false, earlyOps(new(container.Ops). - Dev("/dev", false), + Dev(container.MustAbs("/dev"), false), ), earlyMnt( ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), @@ -119,20 +118,20 @@ var containerTestCases = []struct { {"overlay", true, false, false, true, func(t *testing.T) (*container.Ops, context.Context) { - tempDir := t.TempDir() + tempDir := container.MustAbs(t.TempDir()) lower0, lower1, upper, work := - path.Join(tempDir, "lower0"), - path.Join(tempDir, "lower1"), - path.Join(tempDir, "upper"), - path.Join(tempDir, "work") - for _, name := range []string{lower0, lower1, upper, work} { - if err := os.Mkdir(name, 0755); err != nil { + tempDir.Append("lower0"), + tempDir.Append("lower1"), + tempDir.Append("upper"), + tempDir.Append("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) } } return new(container.Ops). - Overlay(hst.Tmp, upper, work, lower0, lower1), + Overlay(hst.AbsTmp, upper, work, lower0, lower1), context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(), testVal("lower1"), lower1), testVal("lower0"), lower0), @@ -143,12 +142,12 @@ var containerTestCases = []struct { return []*vfs.MountInfoEntry{ ent("/", hst.Tmp, "rw", "overlay", "overlay", "rw,lowerdir="+ - container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(string))+":"+ - container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(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")).(string))+ + container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+ ",workdir="+ - container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(string))+ + container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+ ",redirect_dir=nofollow,uuid=on,userxattr"), } }, @@ -156,18 +155,18 @@ var containerTestCases = []struct { {"overlay ephemeral", true, false, false, true, func(t *testing.T) (*container.Ops, context.Context) { - tempDir := t.TempDir() + tempDir := container.MustAbs(t.TempDir()) lower0, lower1 := - path.Join(tempDir, "lower0"), - path.Join(tempDir, "lower1") - for _, name := range []string{lower0, lower1} { - if err := os.Mkdir(name, 0755); err != nil { + tempDir.Append("lower0"), + tempDir.Append("lower1") + for _, a := range []*container.Absolute{lower0, lower1} { + if err := os.Mkdir(a.String(), 0755); err != nil { t.Fatalf("Mkdir: error = %v", err) } } return new(container.Ops). - OverlayEphemeral(hst.Tmp, lower0, lower1), + OverlayEphemeral(hst.AbsTmp, lower0, lower1), t.Context() }, func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry { @@ -180,17 +179,17 @@ var containerTestCases = []struct { {"overlay readonly", true, false, false, true, func(t *testing.T) (*container.Ops, context.Context) { - tempDir := t.TempDir() + tempDir := container.MustAbs(t.TempDir()) lower0, lower1 := - path.Join(tempDir, "lower0"), - path.Join(tempDir, "lower1") - for _, name := range []string{lower0, lower1} { - if err := os.Mkdir(name, 0755); err != nil { + tempDir.Append("lower0"), + tempDir.Append("lower1") + for _, a := range []*container.Absolute{lower0, lower1} { + if err := os.Mkdir(a.String(), 0755); err != nil { t.Fatalf("Mkdir: error = %v", err) } } return new(container.Ops). - OverlayReadonly(hst.Tmp, lower0, lower1), + OverlayReadonly(hst.AbsTmp, lower0, lower1), context.WithValue(context.WithValue(t.Context(), testVal("lower1"), lower1), testVal("lower0"), lower0) @@ -199,8 +198,8 @@ var containerTestCases = []struct { return []*vfs.MountInfoEntry{ ent("/", hst.Tmp, "rw", "overlay", "overlay", "ro,lowerdir="+ - container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(string))+":"+ - container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(string))+ + container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+ + container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+ ",redirect_dir=nofollow,userxattr"), } }, @@ -252,7 +251,7 @@ func TestContainer(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) defer cancel() - var libPaths []string + var libPaths []*container.Absolute c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i)) c.Uid = tc.uid c.Gid = tc.gid @@ -273,11 +272,11 @@ func TestContainer(t *testing.T) { c.HostNet = tc.net c. - Readonly(pathReadonly, 0755). - Tmpfs("/tmp", 0, 0755). - Place("/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("/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)) @@ -286,9 +285,9 @@ func TestContainer(t *testing.T) { // Bind(os.Args[0], helperInnerPath, 0) ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore), ) - for _, name := range libPaths { + for _, a := range libPaths { // Bind(name, name, 0) - mnt = append(mnt, ent(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore)) + mnt = append(mnt, ent(ignore, a.String(), "ro,nosuid,nodev,relatime", ignore, ignore, ignore)) } mnt = append(mnt, wantMnt...) mnt = append(mnt, @@ -308,10 +307,10 @@ func TestContainer(t *testing.T) { _, _ = output.WriteTo(os.Stdout) t.Fatalf("cannot serialise expected mount points: %v", err) } - c.Place(pathWantMnt, want.Bytes()) + c.Place(container.MustAbs(pathWantMnt), want.Bytes()) if tc.ro { - c.Remount("/", syscall.MS_RDONLY) + c.Remount(container.MustAbs("/"), syscall.MS_RDONLY) } if err := c.Start(); err != nil { @@ -392,7 +391,7 @@ func testContainerCancel( } func TestContainerString(t *testing.T) { - c := container.NewCommand(t.Context(), "/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( seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY, diff --git a/container/init.go b/container/init.go index 0ede1d7..03ec883 100644 --- a/container/init.go +++ b/container/init.go @@ -268,12 +268,12 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) { } Umask(oldmask) - cmd := exec.Command(params.Path) + cmd := exec.Command(params.Path.String()) cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Args = params.Args cmd.Env = params.Env cmd.ExtraFiles = extraFiles - cmd.Dir = params.Dir + cmd.Dir = params.Dir.String() msg.Verbosef("starting initial program %s", params.Path) if err := cmd.Start(); err != nil { diff --git a/container/init_test.go b/container/init_test.go index 7716458..9e6832d 100644 --- a/container/init_test.go +++ b/container/init_test.go @@ -21,6 +21,10 @@ const ( helperInnerPath = "/usr/bin/helper" ) +var ( + absHelperInnerPath = container.MustAbs(helperInnerPath) +) + var helperCommands []func(c command.Command) func TestMain(m *testing.M) { @@ -46,10 +50,10 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func helperNewContainerLibPaths(ctx context.Context, libPaths *[]string, args ...string) (c *container.Container) { - c = container.NewCommand(ctx, helperInnerPath, "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(os.Args[0], helperInnerPath, 0) + c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0) // in case test has cgo enabled if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil { @@ -65,5 +69,5 @@ func helperNewContainerLibPaths(ctx context.Context, libPaths *[]string, args .. } func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) { - return helperNewContainerLibPaths(ctx, new([]string), args...) + return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...) } diff --git a/container/ops.go b/container/ops.go index 927c60a..be30fcc 100644 --- a/container/ops.go +++ b/container/ops.go @@ -47,22 +47,22 @@ func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) } func init() { gob.Register(new(RemountOp)) } // Remount appends an [Op] that applies [RemountOp.Flags] on container path [RemountOp.Target]. -func (f *Ops) Remount(target string, flags uintptr) *Ops { +func (f *Ops) Remount(target *Absolute, flags uintptr) *Ops { *f = append(*f, &RemountOp{target, flags}) return f } type RemountOp struct { - Target string + Target *Absolute Flags uintptr } func (*RemountOp) early(*Params) error { return nil } func (r *RemountOp) apply(*Params) error { - if !path.IsAbs(r.Target) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", r.Target)) + if r.Target == nil { + return EBADE } - return wrapErrSuffix(hostProc.remount(toSysroot(r.Target), r.Flags), + return wrapErrSuffix(hostProc.remount(toSysroot(r.Target.String()), r.Flags), fmt.Sprintf("cannot remount %q:", r.Target)) } @@ -73,13 +73,13 @@ func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Targe 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 string, flags int) *Ops { - *f = append(*f, &BindMountOp{source, "", target, flags}) +func (f *Ops) Bind(source, target *Absolute, flags int) *Ops { + *f = append(*f, &BindMountOp{nil, source, target, flags}) return f } type BindMountOp struct { - Source, sourceFinal, Target string + sourceFinal, Source, Target *Absolute Flags int } @@ -94,24 +94,24 @@ const ( ) func (b *BindMountOp) early(*Params) error { - if !path.IsAbs(b.Source) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", b.Source)) + if b.Source == nil || b.Target == nil { + return EBADE } - if v, err := filepath.EvalSymlinks(b.Source); err != nil { + if pathname, err := filepath.EvalSymlinks(b.Source.String()); err != nil { if os.IsNotExist(err) && b.Flags&BindOptional != 0 { - b.sourceFinal = "\x00" + // leave sourceFinal as nil return nil } return wrapErrSelf(err) } else { - b.sourceFinal = v - return nil + b.sourceFinal, err = NewAbs(pathname) + return err } } func (b *BindMountOp) apply(*Params) error { - if b.sourceFinal == "\x00" { + if b.sourceFinal == nil { if b.Flags&BindOptional == 0 { // unreachable return EBADE @@ -119,12 +119,8 @@ func (b *BindMountOp) apply(*Params) error { return nil } - if !path.IsAbs(b.sourceFinal) || !path.IsAbs(b.Target) { - return msg.WrapErr(EBADE, "path is not absolute") - } - - source := toHost(b.sourceFinal) - target := toSysroot(b.Target) + source := toHost(b.sourceFinal.String()) + target := toSysroot(b.Target.String()) // this perm value emulates bwrap behaviour as it clears bits from 0755 based on // op->perms which is never set for any bind setup op so always results in 0700 @@ -161,60 +157,62 @@ func (b *BindMountOp) String() string { func init() { gob.Register(new(MountProcOp)) } // Proc appends an [Op] that mounts a private instance of proc. -func (f *Ops) Proc(dest string) *Ops { - *f = append(*f, MountProcOp(dest)) +func (f *Ops) Proc(target *Absolute) *Ops { + *f = append(*f, &MountProcOp{target}) return f } -type MountProcOp string +type MountProcOp struct { + Target *Absolute +} -func (p MountProcOp) early(*Params) error { return nil } -func (p MountProcOp) apply(params *Params) error { - v := string(p) - - if !path.IsAbs(v) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v)) +func (p *MountProcOp) early(*Params) error { return nil } +func (p *MountProcOp) apply(params *Params) error { + if p.Target == nil { + return EBADE } - - target := toSysroot(v) + target := toSysroot(p.Target.String()) if err := os.MkdirAll(target, params.ParentPerm); err != nil { return wrapErrSelf(err) } return wrapErrSuffix(Mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString), - fmt.Sprintf("cannot mount proc on %q:", v)) + fmt.Sprintf("cannot mount proc on %q:", p.Target.String())) } -func (p MountProcOp) Is(op Op) bool { vp, ok := op.(MountProcOp); return ok && p == vp } -func (MountProcOp) prefix() string { return "mounting" } -func (p MountProcOp) String() string { return fmt.Sprintf("proc on %q", string(p)) } +func (p *MountProcOp) Is(op Op) bool { + vp, ok := op.(*MountProcOp) + return ok && ((p == nil && vp == nil) || p == vp) +} +func (*MountProcOp) prefix() string { return "mounting" } +func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) } func init() { gob.Register(new(MountDevOp)) } // Dev appends an [Op] that mounts a subset of host /dev. -func (f *Ops) Dev(dest string, mqueue bool) *Ops { - *f = append(*f, &MountDevOp{dest, mqueue, false}) +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(dest string, mqueue bool) *Ops { - *f = append(*f, &MountDevOp{dest, mqueue, true}) +func (f *Ops) DevWritable(target *Absolute, mqueue bool) *Ops { + *f = append(*f, &MountDevOp{target, mqueue, true}) return f } type MountDevOp struct { - Target string + Target *Absolute Mqueue bool Write bool } func (d *MountDevOp) early(*Params) error { return nil } func (d *MountDevOp) apply(params *Params) error { - if !path.IsAbs(d.Target) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", d.Target)) + if d.Target == nil { + return EBADE } - target := toSysroot(d.Target) + target := toSysroot(d.Target.String()) if err := mountTmpfs(SourceTmpfsDevtmpfs, target, MS_NOSUID|MS_NODEV, 0, params.ParentPerm); err != nil { return err @@ -314,20 +312,20 @@ func (d *MountDevOp) String() string { func init() { gob.Register(new(MountTmpfsOp)) } // Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path]. -func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops { - *f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, dest, MS_NOSUID | MS_NODEV, size, perm}) +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(dest string, perm os.FileMode) *Ops { - *f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, dest, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm}) +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 } type MountTmpfsOp struct { FSName string - Path string + Path *Absolute Flags uintptr Size int Perm os.FileMode @@ -335,13 +333,13 @@ type MountTmpfsOp struct { func (t *MountTmpfsOp) early(*Params) error { return nil } func (t *MountTmpfsOp) apply(*Params) error { - if !path.IsAbs(t.Path) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) + if t.Path == nil { + return EBADE } if t.Size < 0 || t.Size > math.MaxUint>>1 { return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size)) } - return mountTmpfs(t.FSName, toSysroot(t.Path), t.Flags, t.Size, t.Perm) + return mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm) } func (t *MountTmpfsOp) Is(op Op) bool { vt, ok := op.(*MountTmpfsOp); return ok && *t == *vt } @@ -351,7 +349,7 @@ func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d func init() { gob.Register(new(MountOverlayOp)) } // Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]. -func (f *Ops) Overlay(target, state, work string, layers ...string) *Ops { +func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops { *f = append(*f, &MountOverlayOp{ Target: target, Lower: layers, @@ -363,94 +361,94 @@ func (f *Ops) Overlay(target, state, work string, layers ...string) *Ops { // OverlayEphemeral appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target] // with an ephemeral upperdir and workdir. -func (f *Ops) OverlayEphemeral(target string, layers ...string) *Ops { - return f.Overlay(target, SourceTmpfsEphemeral, zeroString, 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 string, layers ...string) *Ops { - return f.Overlay(target, zeroString, zeroString, layers...) +func (f *Ops) OverlayReadonly(target *Absolute, layers ...*Absolute) *Ops { + return f.Overlay(target, nil, nil, layers...) } type MountOverlayOp struct { - Target string + Target *Absolute - // formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early; - Lower []string - // formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early; + // Any filesystem, does not need to be on a writable filesystem. + Lower []*Absolute + // formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early + lower []string + // The upperdir is normally on a writable filesystem. // - // If Work is an empty string and Upper holds the special value [SourceTmpfsEphemeral], + // If Work is nil and Upper holds the special value [FHSRoot], // an ephemeral upperdir and workdir will be set up. // // If both Work and Upper are empty strings, upperdir and workdir is omitted and the overlay is mounted readonly. - Upper string - // formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early; - Work string + 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 *Absolute + // formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early + work string ephemeral bool } func (o *MountOverlayOp) early(*Params) error { - if o.Work == zeroString { - switch o.Upper { - case SourceTmpfsEphemeral: // ephemeral + if o.Work == nil && o.Upper != nil { + switch o.Upper.String() { + case FHSRoot: // ephemeral o.ephemeral = true // intermediate root not yet available - case zeroString: // readonly - default: return msg.WrapErr(EINVAL, fmt.Sprintf("upperdir has unexpected value %q", o.Upper)) } } + // readonly handled in apply if !o.ephemeral { - if o.Upper != o.Work && (o.Upper == zeroString || o.Work == zeroString) { + if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) { // unreachable return msg.WrapErr(ENOTRECOVERABLE, "impossible overlay state reached") } - if o.Upper != zeroString { - if !path.IsAbs(o.Upper) { - return msg.WrapErr(EBADE, fmt.Sprintf("upperdir %q is not absolute", o.Upper)) - } - if v, err := filepath.EvalSymlinks(o.Upper); err != nil { + if o.Upper != nil { + if v, err := filepath.EvalSymlinks(o.Upper.String()); err != nil { return wrapErrSelf(err) } else { - o.Upper = escapeOverlayDataSegment(toHost(v)) + o.upper = escapeOverlayDataSegment(toHost(v)) } } - if o.Work != zeroString { - if !path.IsAbs(o.Work) { - return msg.WrapErr(EBADE, fmt.Sprintf("workdir %q is not absolute", o.Work)) - } - if v, err := filepath.EvalSymlinks(o.Work); err != nil { + if o.Work != nil { + if v, err := filepath.EvalSymlinks(o.Work.String()); err != nil { return wrapErrSelf(err) } else { - o.Work = escapeOverlayDataSegment(toHost(v)) + o.work = escapeOverlayDataSegment(toHost(v)) } } } - for i := range o.Lower { - if !path.IsAbs(o.Lower[i]) { - return msg.WrapErr(EBADE, fmt.Sprintf("lowerdir %q is not absolute", o.Lower[i])) + o.lower = make([]string, len(o.Lower)) + for i, a := range o.Lower { + if a == nil { + return EBADE } - if v, err := filepath.EvalSymlinks(o.Lower[i]); err != nil { + if v, err := filepath.EvalSymlinks(a.String()); err != nil { return wrapErrSelf(err) } else { - o.Lower[i] = escapeOverlayDataSegment(toHost(v)) + o.lower[i] = escapeOverlayDataSegment(toHost(v)) } } return nil } func (o *MountOverlayOp) apply(params *Params) error { - if !path.IsAbs(o.Target) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", o.Target)) + if o.Target == nil { + return EBADE } - target := toSysroot(o.Target) + target := toSysroot(o.Target.String()) if err := os.MkdirAll(target, params.ParentPerm); err != nil { return wrapErrSelf(err) } @@ -458,17 +456,17 @@ func (o *MountOverlayOp) apply(params *Params) error { if o.ephemeral { var err error // these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed - if o.Upper, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil { + if o.upper, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil { return wrapErrSelf(err) } - if o.Work, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil { + if o.work, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil { return wrapErrSelf(err) } } options := make([]string, 0, 4) - if o.Upper == zeroString && o.Work == zeroString { // readonly + if o.upper == zeroString && o.work == zeroString { // readonly if len(o.Lower) < 2 { return msg.WrapErr(EINVAL, "readonly overlay requires at least two lowerdir") } @@ -478,11 +476,11 @@ func (o *MountOverlayOp) apply(params *Params) error { return msg.WrapErr(EINVAL, "overlay requires at least one lowerdir") } options = append(options, - OptionOverlayUpperdir+"="+o.Upper, - OptionOverlayWorkdir+"="+o.Work) + OptionOverlayUpperdir+"="+o.upper, + OptionOverlayWorkdir+"="+o.work) } options = append(options, - OptionOverlayLowerdir+"="+strings.Join(o.Lower, SpecialOverlayPath), + OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath), OptionOverlayUserxattr) return wrapErrSuffix(Mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)), @@ -505,70 +503,73 @@ func (o *MountOverlayOp) String() string { func init() { gob.Register(new(SymlinkOp)) } // Link appends an [Op] that creates a symlink in the container filesystem. -func (f *Ops) Link(target, linkName string) *Ops { - *f = append(*f, &SymlinkOp{target, linkName}) +func (f *Ops) Link(target *Absolute, linkName string, dereference bool) *Ops { + *f = append(*f, &SymlinkOp{target, linkName, dereference}) return f } -type SymlinkOp [2]string +type SymlinkOp struct { + Target *Absolute + // LinkName is an arbitrary uninterpreted pathname. + LinkName string + + // Dereference causes LinkName to be dereferenced during early. + Dereference bool +} func (l *SymlinkOp) early(*Params) error { - if strings.HasPrefix(l[0], "*") { - l[0] = l[0][1:] - if !path.IsAbs(l[0]) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[0])) + if l.Dereference { + if !isAbs(l.LinkName) { + return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l.LinkName)) } - if name, err := os.Readlink(l[0]); err != nil { + if name, err := os.Readlink(l.LinkName); err != nil { return wrapErrSelf(err) } else { - l[0] = name + l.LinkName = name } } return nil } -func (l *SymlinkOp) apply(params *Params) error { - // symlink target is an arbitrary path value, so only validate link name here - if !path.IsAbs(l[1]) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[1])) - } - target := toSysroot(l[1]) +func (l *SymlinkOp) apply(params *Params) error { + if l.Target == nil { + return EBADE + } + target := toSysroot(l.Target.String()) if err := os.MkdirAll(path.Dir(target), params.ParentPerm); err != nil { return wrapErrSelf(err) } - if err := os.Symlink(l[0], target); err != nil { + if err := os.Symlink(l.LinkName, target); err != nil { return wrapErrSelf(err) } return nil } -func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl } -func (*SymlinkOp) prefix() string { return "creating" } -func (l *SymlinkOp) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) } +func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl } +func (*SymlinkOp) prefix() string { return "creating" } +func (l *SymlinkOp) String() string { + return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName) +} func init() { gob.Register(new(MkdirOp)) } // Mkdir appends an [Op] that creates a directory in the container filesystem. -func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops { - *f = append(*f, &MkdirOp{dest, perm}) +func (f *Ops) Mkdir(name *Absolute, perm os.FileMode) *Ops { + *f = append(*f, &MkdirOp{name, perm}) return f } type MkdirOp struct { - Path string + Path *Absolute Perm os.FileMode } func (m *MkdirOp) early(*Params) error { return nil } func (m *MkdirOp) apply(*Params) error { - if !path.IsAbs(m.Path) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", m.Path)) + if m.Path == nil { + return EBADE } - - if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil { - return wrapErrSelf(err) - } - return nil + return wrapErrSelf(os.MkdirAll(toSysroot(m.Path.String()), m.Perm)) } func (m *MkdirOp) Is(op Op) bool { vm, ok := op.(*MkdirOp); return ok && m == vm } @@ -578,10 +579,13 @@ func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m 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 string, data []byte) *Ops { *f = append(*f, &TmpfileOp{name, data}); return f } +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 string, dataP **[]byte) *Ops { +func (f *Ops) PlaceP(name *Absolute, dataP **[]byte) *Ops { t := &TmpfileOp{Path: name} *dataP = &t.Data @@ -590,14 +594,14 @@ func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops { } type TmpfileOp struct { - Path string + Path *Absolute Data []byte } func (t *TmpfileOp) early(*Params) error { return nil } func (t *TmpfileOp) apply(params *Params) error { - if !path.IsAbs(t.Path) { - return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) + if t.Path == nil { + return EBADE } var tmpPath string @@ -613,7 +617,7 @@ func (t *TmpfileOp) apply(params *Params) error { tmpPath = f.Name() } - target := toSysroot(t.Path) + target := toSysroot(t.Path.String()) if err := ensureFile(target, 0444, params.ParentPerm); err != nil { return err } else if err = hostProc.bindMount( diff --git a/container/path.go b/container/path.go index 28a56a1..fe6d6d4 100644 --- a/container/path.go +++ b/container/path.go @@ -13,6 +13,8 @@ import ( "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 = "/" @@ -49,6 +51,38 @@ const ( 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} + + // AbsNonexistent is [Nonexistent] as [Absolute]. + AbsNonexistent = &Absolute{Nonexistent} +) + const ( // Nonexistent is a path that cannot exist. // /proc is chosen because a system with covered /proc is unsupported by this package. diff --git a/helper/container.go b/helper/container.go index 7501b28..a3b2257 100644 --- a/helper/container.go +++ b/helper/container.go @@ -26,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, pathname.String(), name, args...) + h.Container = container.NewCommand(ctx, pathname, name, args...) h.WaitDelay = WaitDelay if cmdF != nil { cmdF(h.Container) diff --git a/helper/container_test.go b/helper/container_test.go index 56adb8c..2d1c256 100644 --- a/helper/container_test.go +++ b/helper/container_test.go @@ -33,7 +33,10 @@ func TestContainer(t *testing.T) { testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper { return helper.New(ctx, container.MustAbs(os.Args[0]), "helper", argsWt, stat, argF, func(z *container.Container) { setOutput(&z.Stdout, &z.Stderr) - z.Bind("/", "/", 0).Proc("/proc").Dev("/dev", true) + z. + Bind(container.AbsFHSRoot, container.AbsFHSRoot, 0). + Proc(container.AbsFHSProc). + Dev(container.AbsFHSDev, true) }, nil) }) }) diff --git a/hst/config.go b/hst/config.go index 8c883c9..73fb21b 100644 --- a/hst/config.go +++ b/hst/config.go @@ -2,12 +2,15 @@ package hst import ( + "hakurei.app/container" "hakurei.app/system" "hakurei.app/system/dbus" ) const Tmp = "/.hakurei" +var AbsTmp = container.MustAbs(Tmp) + // Config is used to seal an app implementation. type Config struct { // reverse-DNS style arbitrary identifier string from config; @@ -16,7 +19,7 @@ type Config struct { ID string `json:"id"` // absolute path to executable file - Path string `json:"path,omitempty"` + Path *container.Absolute `json:"path,omitempty"` // final args passed to container init Args []string `json:"args"` @@ -35,12 +38,12 @@ type Config struct { // passwd username in container, defaults to passwd name of target uid or chronos Username string `json:"username,omitempty"` - // absolute path to shell, empty for host shell - Shell string `json:"shell,omitempty"` + // absolute path to shell + Shell *container.Absolute `json:"shell"` // absolute path to home directory in the init mount namespace - Data string `json:"data"` - // directory to enter and use as home in the container mount namespace, empty for Data - Dir string `json:"dir"` + Data *container.Absolute `json:"data"` + // directory to enter and use as home in the container mount namespace, nil for Data + Dir *container.Absolute `json:"dir,omitempty"` // extra acl ops, dispatches before container init ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"` @@ -55,21 +58,24 @@ type Config struct { // ExtraPermConfig describes an acl update op. type ExtraPermConfig struct { - Ensure bool `json:"ensure,omitempty"` - Path string `json:"path"` - Read bool `json:"r,omitempty"` - Write bool `json:"w,omitempty"` - Execute bool `json:"x,omitempty"` + Ensure bool `json:"ensure,omitempty"` + Path *container.Absolute `json:"path"` + Read bool `json:"r,omitempty"` + Write bool `json:"w,omitempty"` + Execute bool `json:"x,omitempty"` } func (e *ExtraPermConfig) String() string { - buf := make([]byte, 0, 5+len(e.Path)) + if e.Path == nil { + return "" + } + buf := make([]byte, 0, 5+len(e.Path.String())) buf = append(buf, '-', '-', '-') if e.Ensure { buf = append(buf, '+') } buf = append(buf, ':') - buf = append(buf, []byte(e.Path)...) + buf = append(buf, []byte(e.Path.String())...) if e.Read { buf[0] = 'r' } diff --git a/hst/container.go b/hst/container.go index ebce07b..0b51e10 100644 --- a/hst/container.go +++ b/hst/container.go @@ -3,14 +3,11 @@ package hst import ( "time" + "hakurei.app/container" "hakurei.app/container/seccomp" ) const ( - // SourceTmpfs causes tmpfs to be mounted on [FilesystemConfig.Dst] - // when assigned to [FilesystemConfig.Src]. - SourceTmpfs = "tmpfs" - // TmpfsPerm is the permission bits for tmpfs mount points // configured through [FilesystemConfig]. TmpfsPerm = 0755 @@ -55,18 +52,18 @@ type ( // pass through all devices Device bool `json:"device,omitempty"` // container host filesystem bind mounts - Filesystem []*FilesystemConfig `json:"filesystem"` + Filesystem []FilesystemConfig `json:"filesystem"` // create symlinks inside container filesystem - Link [][2]string `json:"symlink"` + Link []LinkConfig `json:"symlink"` // automatically bind mount top-level directories to container root; // the zero value disables this behaviour - AutoRoot string `json:"auto_root,omitempty"` + AutoRoot *container.Absolute `json:"auto_root,omitempty"` // extra flags for AutoRoot RootFlags int `json:"root_flags,omitempty"` // read-only /etc directory - Etc string `json:"etc,omitempty"` + Etc *container.Absolute `json:"etc,omitempty"` // automatically set up /etc symlinks AutoEtc bool `json:"auto_etc"` } @@ -74,9 +71,9 @@ type ( // FilesystemConfig is an abstract representation of a bind mount. FilesystemConfig struct { // mount point in container, same as src if empty - Dst string `json:"dst,omitempty"` + Dst *container.Absolute `json:"dst,omitempty"` // host filesystem path to make available to the container - Src string `json:"src"` + Src *container.Absolute `json:"src"` // do not mount filesystem read-only Write bool `json:"write,omitempty"` // do not disable device files @@ -84,4 +81,12 @@ type ( // fail if the bind mount cannot be established for any reason Must bool `json:"require,omitempty"` } + + LinkConfig struct { + // symlink target in container + Target *container.Absolute `json:"target"` + // linkname the symlink points to; + // prepend '*' to dereference an absolute pathname on host + Linkname string `json:"linkname"` + } ) diff --git a/hst/paths.go b/hst/paths.go index 31c14df..7410d7a 100644 --- a/hst/paths.go +++ b/hst/paths.go @@ -1,11 +1,15 @@ package hst +import "hakurei.app/container" + // Paths contains environment-dependent paths used by hakurei. type Paths struct { + // temporary directory returned by [os.TempDir] (usually `/tmp`) + TempDir *container.Absolute `json:"temp_dir"` // path to shared directory (usually `/tmp/hakurei.%d`) - SharePath string `json:"share_path"` + SharePath *container.Absolute `json:"share_path"` // XDG_RUNTIME_DIR value (usually `/run/user/%d`) - RuntimePath string `json:"runtime_path"` + RuntimePath *container.Absolute `json:"runtime_path"` // application runtime directory (usually `/run/user/%d/hakurei`) - RunDirPath string `json:"run_dir_path"` + RunDirPath *container.Absolute `json:"run_dir_path"` } diff --git a/hst/template.go b/hst/template.go index 32d84cd..dda43a0 100644 --- a/hst/template.go +++ b/hst/template.go @@ -12,7 +12,7 @@ func Template() *Config { return &Config{ ID: "org.chromium.Chromium", - Path: container.FHSRun + "current-system/sw/bin/chromium", + Path: container.AbsFHSRun.Append("current-system/sw/bin/chromium"), Args: []string{ "chromium", "--ignore-gpu-blocklist", @@ -46,12 +46,12 @@ func Template() *Config { DirectWayland: false, Username: "chronos", - Shell: container.FHSRun + "current-system/sw/bin/zsh", - Data: container.FHSVarLib + "hakurei/u0/org.chromium.Chromium", - Dir: "/data/data/org.chromium.Chromium", + Shell: container.AbsFHSRun.Append("current-system/sw/bin/zsh"), + Data: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"), + Dir: container.MustAbs("/data/data/org.chromium.Chromium"), ExtraPerms: []*ExtraPermConfig{ - {Path: container.FHSVarLib + "hakurei/u0", Ensure: true, Execute: true}, - {Path: container.FHSVarLib + "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, @@ -77,20 +77,20 @@ func Template() *Config { "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT", }, - Filesystem: []*FilesystemConfig{ - {Dst: container.FHSTmp, Src: SourceTmpfs, Write: true}, - {Src: "/nix/store"}, - {Src: container.FHSRun + "current-system"}, - {Src: container.FHSRun + "opengl-driver"}, - {Src: container.FHSVar + "db/nix-channels"}, - {Src: container.FHSVarLib + "hakurei/u0/org.chromium.Chromium", - Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true}, - {Src: container.FHSDev + "dri", Device: true}, + Filesystem: []FilesystemConfig{ + {Dst: container.AbsFHSTmp, Src: container.AbsNonexistent, Write: true}, + {Src: container.MustAbs("/nix/store")}, + {Src: container.AbsFHSRun.Append("current-system")}, + {Src: container.AbsFHSRun.Append("opengl-driver")}, + {Src: container.AbsFHSVar.Append("db/nix-channels")}, + {Src: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"), + Dst: container.MustAbs("/data/data/org.chromium.Chromium"), Write: true, Must: true}, + {Src: container.AbsFHSDev.Append("dri"), Device: true}, }, - Link: [][2]string{{container.FHSRunUser + "65534", container.FHSRunUser + "150"}}, - AutoRoot: container.FHSVarLib + "hakurei/base/org.debian", + Link: []LinkConfig{{container.AbsFHSRunUser.Append("65534"), container.FHSRunUser + "150"}}, + AutoRoot: container.AbsFHSVarLib.Append("hakurei/base/org.debian"), RootFlags: container.BindWritable, - Etc: container.FHSEtc, + Etc: container.AbsFHSEtc, AutoEtc: true, }, } diff --git a/hst/template_test.go b/hst/template_test.go index cf8ab81..da119d4 100644 --- a/hst/template_test.go +++ b/hst/template_test.go @@ -99,7 +99,7 @@ func TestTemplate(t *testing.T) { "filesystem": [ { "dst": "/tmp/", - "src": "tmpfs", + "src": "/proc/nonexistent", "write": true }, { @@ -126,10 +126,10 @@ func TestTemplate(t *testing.T) { } ], "symlink": [ - [ - "/run/user/65534", - "/run/user/150" - ] + { + "target": "/run/user/65534", + "linkname": "/run/user/150" + } ], "auto_root": "/var/lib/hakurei/base/org.debian", "root_flags": 2, diff --git a/internal/app/app_linux.go b/internal/app/app_linux.go index 89ff6ed..1563f99 100644 --- a/internal/app/app_linux.go +++ b/internal/app/app_linux.go @@ -7,7 +7,6 @@ import ( "hakurei.app/hst" "hakurei.app/internal/app/state" - "hakurei.app/internal/hlog" "hakurei.app/internal/sys" ) @@ -59,10 +58,6 @@ func (a *app) Seal(config *hst.Config) (SealedApp, error) { if a.outcome != nil { panic("app sealed twice") } - if config == nil { - return nil, hlog.WrapErr(ErrConfig, - "attempted to seal app with nil config") - } seal := new(outcome) seal.id = a.id diff --git a/internal/app/app_linux_test.go b/internal/app/app_linux_test.go index 7d572b5..8b96cfb 100644 --- a/internal/app/app_linux_test.go +++ b/internal/app/app_linux_test.go @@ -11,6 +11,7 @@ import ( "hakurei.app/hst" "hakurei.app/internal/app" "hakurei.app/internal/app/state" + "hakurei.app/internal/hlog" "hakurei.app/internal/sys" "hakurei.app/system" ) @@ -36,6 +37,7 @@ func TestApp(t *testing.T) { ) if !t.Run("seal", func(t *testing.T) { if sa, err := a.Seal(tc.config); err != nil { + hlog.PrintBaseError(err, "got generic error:") t.Errorf("Seal: error = %v", err) return } else { diff --git a/internal/app/app_nixos_linux_test.go b/internal/app/app_nixos_linux_test.go index 4b7b3d3..901e825 100644 --- a/internal/app/app_nixos_linux_test.go +++ b/internal/app/app_nixos_linux_test.go @@ -12,21 +12,24 @@ import ( "hakurei.app/system/dbus" ) +func m(pathname string) *container.Absolute { return container.MustAbs(pathname) } + var testCasesNixos = []sealTestCase{ { "nixos chromium direct wayland", new(stubNixOS), &hst.Config{ ID: "org.chromium.Chromium", - Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start", + Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"), Enablements: system.EWayland | system.EDBus | system.EPulse, + Shell: m("/run/current-system/sw/bin/zsh"), Container: &hst.ContainerConfig{ Userns: true, Net: true, MapRealUID: true, Env: nil, AutoEtc: true, - Filesystem: []*hst.FilesystemConfig{ - {Src: "/bin", Must: true}, {Src: "/usr/bin/", Must: true}, - {Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true}, - {Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"}, - {Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true}, + Filesystem: []hst.FilesystemConfig{ + {Src: m("/bin"), Must: true}, {Src: m("/usr/bin/"), Must: true}, + {Src: m("/nix/store"), Must: true}, {Src: m("/run/current-system"), Must: true}, + {Src: m("/sys/block")}, {Src: m("/sys/bus")}, {Src: m("/sys/class")}, {Src: m("/sys/dev")}, {Src: m("/sys/devices")}, + {Src: m("/run/opengl-driver"), Must: true}, {Src: m("/dev/dri"), Device: true}, }, }, SystemBus: &dbus.Config{ @@ -50,7 +53,7 @@ var testCasesNixos = []sealTestCase{ DirectWayland: true, Username: "u0_a1", - Data: "/var/lib/persist/module/hakurei/0/1", + Data: m("/var/lib/persist/module/hakurei/0/1"), Identity: 1, Groups: []string{}, }, state.ID{ @@ -98,8 +101,8 @@ var testCasesNixos = []sealTestCase{ &container.Params{ Uid: 1971, Gid: 100, - Dir: "/var/lib/persist/module/hakurei/0/1", - Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start", + Dir: m("/var/lib/persist/module/hakurei/0/1"), + Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"), Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"}, Env: []string{ "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus", @@ -116,34 +119,34 @@ var testCasesNixos = []sealTestCase{ "XDG_SESSION_TYPE=tty", }, Ops: new(container.Ops). - Proc("/proc/"). - Tmpfs(hst.Tmp, 4096, 0755). - DevWritable("/dev/", true). - Bind("/bin", "/bin", 0). - Bind("/usr/bin/", "/usr/bin/", 0). - Bind("/nix/store", "/nix/store", 0). - Bind("/run/current-system", "/run/current-system", 0). - Bind("/sys/block", "/sys/block", container.BindOptional). - Bind("/sys/bus", "/sys/bus", container.BindOptional). - Bind("/sys/class", "/sys/class", container.BindOptional). - Bind("/sys/dev", "/sys/dev", container.BindOptional). - Bind("/sys/devices", "/sys/devices", container.BindOptional). - Bind("/run/opengl-driver", "/run/opengl-driver", 0). - Bind("/dev/dri", "/dev/dri", container.BindDevice|container.BindWritable|container.BindOptional). - Etc("/etc/", "8e2c76b066dabe574cf073bdb46eb5c1"). - Remount("/dev/", syscall.MS_RDONLY). - Tmpfs("/run/user/", 4096, 0755). - Bind("/tmp/hakurei.1971/runtime/1", "/run/user/1971", container.BindWritable). - Bind("/tmp/hakurei.1971/tmpdir/1", "/tmp/", container.BindWritable). - Bind("/var/lib/persist/module/hakurei/0/1", "/var/lib/persist/module/hakurei/0/1", container.BindWritable). - Place("/etc/passwd", []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")). - Place("/etc/group", []byte("hakurei:x:100:\n")). - Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0). - Bind("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0). - Place(hst.Tmp+"/pulse-cookie", nil). - Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0). - Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0). - Remount("/", syscall.MS_RDONLY), + Proc(m("/proc/")). + Tmpfs(hst.AbsTmp, 4096, 0755). + DevWritable(m("/dev/"), true). + Bind(m("/bin"), m("/bin"), 0). + Bind(m("/usr/bin/"), m("/usr/bin/"), 0). + Bind(m("/nix/store"), m("/nix/store"), 0). + Bind(m("/run/current-system"), m("/run/current-system"), 0). + Bind(m("/sys/block"), m("/sys/block"), container.BindOptional). + Bind(m("/sys/bus"), m("/sys/bus"), container.BindOptional). + Bind(m("/sys/class"), m("/sys/class"), container.BindOptional). + Bind(m("/sys/dev"), m("/sys/dev"), container.BindOptional). + Bind(m("/sys/devices"), m("/sys/devices"), container.BindOptional). + Bind(m("/run/opengl-driver"), m("/run/opengl-driver"), 0). + Bind(m("/dev/dri"), m("/dev/dri"), container.BindDevice|container.BindWritable|container.BindOptional). + Etc(m("/etc/"), "8e2c76b066dabe574cf073bdb46eb5c1"). + Remount(m("/dev/"), syscall.MS_RDONLY). + Tmpfs(m("/run/user/"), 4096, 0755). + Bind(m("/tmp/hakurei.1971/runtime/1"), m("/run/user/1971"), container.BindWritable). + Bind(m("/tmp/hakurei.1971/tmpdir/1"), m("/tmp/"), container.BindWritable). + Bind(m("/var/lib/persist/module/hakurei/0/1"), m("/var/lib/persist/module/hakurei/0/1"), container.BindWritable). + Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")). + Place(m("/etc/group"), []byte("hakurei:x:100:\n")). + Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0). + Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0). + Place(m(hst.Tmp+"/pulse-cookie"), nil). + Bind(m("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0). + Bind(m("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0). + Remount(m("/"), syscall.MS_RDONLY), SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel, HostNet: true, ForwardCancel: true, diff --git a/internal/app/app_pd_linux_test.go b/internal/app/app_pd_linux_test.go index 2dd83b7..cc738ed 100644 --- a/internal/app/app_pd_linux_test.go +++ b/internal/app/app_pd_linux_test.go @@ -16,7 +16,7 @@ import ( var testCasesPd = []sealTestCase{ { "nixos permissive defaults no enablements", new(stubNixOS), - &hst.Config{Username: "chronos", Data: "/home/chronos"}, + &hst.Config{Username: "chronos", Data: m("/home/chronos")}, state.ID{ 0x4a, 0x45, 0x0b, 0x65, 0x96, 0xd7, 0xbc, 0x15, @@ -30,8 +30,8 @@ var testCasesPd = []sealTestCase{ Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute). Ensure("/tmp/hakurei.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute), &container.Params{ - Dir: "/home/chronos", - Path: "/run/current-system/sw/bin/zsh", + Dir: m("/home/chronos"), + Path: m("/run/current-system/sw/bin/zsh"), Args: []string{"/run/current-system/sw/bin/zsh"}, Env: []string{ "HOME=/home/chronos", @@ -43,23 +43,23 @@ var testCasesPd = []sealTestCase{ "XDG_SESSION_TYPE=tty", }, Ops: new(container.Ops). - Root("/", "4a450b6596d7bc15bd01780eb9a607ac", container.BindWritable). - Proc("/proc/"). - Tmpfs(hst.Tmp, 4096, 0755). - DevWritable("/dev/", true). - Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional). - Readonly("/var/run/nscd", 0755). - Tmpfs("/run/user/1971", 8192, 0755). - Tmpfs("/run/dbus", 8192, 0755). - Etc("/etc/", "4a450b6596d7bc15bd01780eb9a607ac"). - Remount("/dev/", syscall.MS_RDONLY). - Tmpfs("/run/user/", 4096, 0755). - Bind("/tmp/hakurei.1971/runtime/0", "/run/user/65534", container.BindWritable). - Bind("/tmp/hakurei.1971/tmpdir/0", "/tmp/", container.BindWritable). - Bind("/home/chronos", "/home/chronos", container.BindWritable). - Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). - Place("/etc/group", []byte("hakurei:x:65534:\n")). - Remount("/", syscall.MS_RDONLY), + Root(m("/"), "4a450b6596d7bc15bd01780eb9a607ac", container.BindWritable). + Proc(m("/proc/")). + Tmpfs(hst.AbsTmp, 4096, 0755). + DevWritable(m("/dev/"), true). + Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional). + Readonly(m("/var/run/nscd"), 0755). + Tmpfs(m("/run/user/1971"), 8192, 0755). + Tmpfs(m("/run/dbus"), 8192, 0755). + Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac"). + Remount(m("/dev/"), syscall.MS_RDONLY). + Tmpfs(m("/run/user/"), 4096, 0755). + Bind(m("/tmp/hakurei.1971/runtime/0"), m("/run/user/65534"), container.BindWritable). + Bind(m("/tmp/hakurei.1971/tmpdir/0"), m("/tmp/"), container.BindWritable). + Bind(m("/home/chronos"), m("/home/chronos"), container.BindWritable). + Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). + Place(m("/etc/group"), []byte("hakurei:x:65534:\n")). + Remount(m("/"), syscall.MS_RDONLY), SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, HostNet: true, RetainSession: true, @@ -74,7 +74,7 @@ var testCasesPd = []sealTestCase{ Identity: 9, Groups: []string{"video"}, Username: "chronos", - Data: "/home/chronos", + Data: m("/home/chronos"), SessionBus: &dbus.Config{ Talk: []string{ "org.freedesktop.Notifications", @@ -160,8 +160,8 @@ var testCasesPd = []sealTestCase{ UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write). UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write), &container.Params{ - Dir: "/home/chronos", - Path: "/run/current-system/sw/bin/zsh", + Dir: m("/home/chronos"), + Path: m("/run/current-system/sw/bin/zsh"), Args: []string{"zsh", "-c", "exec chromium "}, Env: []string{ "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", @@ -178,29 +178,29 @@ var testCasesPd = []sealTestCase{ "XDG_SESSION_TYPE=tty", }, Ops: new(container.Ops). - Root("/", "ebf083d1b175911782d413369b64ce7c", container.BindWritable). - Proc("/proc/"). - Tmpfs(hst.Tmp, 4096, 0755). - DevWritable("/dev/", true). - Bind("/dev/dri", "/dev/dri", container.BindWritable|container.BindDevice|container.BindOptional). - Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional). - Readonly("/var/run/nscd", 0755). - Tmpfs("/run/user/1971", 8192, 0755). - Tmpfs("/run/dbus", 8192, 0755). - Etc("/etc/", "ebf083d1b175911782d413369b64ce7c"). - Remount("/dev/", syscall.MS_RDONLY). - Tmpfs("/run/user/", 4096, 0755). - Bind("/tmp/hakurei.1971/runtime/9", "/run/user/65534", container.BindWritable). - Bind("/tmp/hakurei.1971/tmpdir/9", "/tmp/", container.BindWritable). - Bind("/home/chronos", "/home/chronos", container.BindWritable). - Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). - Place("/etc/group", []byte("hakurei:x:65534:\n")). - Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0", 0). - Bind("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0). - Place(hst.Tmp+"/pulse-cookie", nil). - Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0). - Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0). - Remount("/", syscall.MS_RDONLY), + Root(m("/"), "ebf083d1b175911782d413369b64ce7c", container.BindWritable). + Proc(m("/proc/")). + Tmpfs(hst.AbsTmp, 4096, 0755). + DevWritable(m("/dev/"), true). + Bind(m("/dev/dri"), m("/dev/dri"), container.BindWritable|container.BindDevice|container.BindOptional). + Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional). + Readonly(m("/var/run/nscd"), 0755). + Tmpfs(m("/run/user/1971"), 8192, 0755). + Tmpfs(m("/run/dbus"), 8192, 0755). + Etc(m("/etc/"), "ebf083d1b175911782d413369b64ce7c"). + Remount(m("/dev/"), syscall.MS_RDONLY). + Tmpfs(m("/run/user/"), 4096, 0755). + Bind(m("/tmp/hakurei.1971/runtime/9"), m("/run/user/65534"), container.BindWritable). + Bind(m("/tmp/hakurei.1971/tmpdir/9"), m("/tmp/"), container.BindWritable). + Bind(m("/home/chronos"), m("/home/chronos"), container.BindWritable). + Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). + Place(m("/etc/group"), []byte("hakurei:x:65534:\n")). + Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0). + Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0). + Place(m(hst.Tmp+"/pulse-cookie"), nil). + Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0). + Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0). + Remount(m("/"), syscall.MS_RDONLY), SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, HostNet: true, RetainSession: true, diff --git a/internal/app/app_stub_linux_test.go b/internal/app/app_stub_linux_test.go index 0efc585..f386eb0 100644 --- a/internal/app/app_stub_linux_test.go +++ b/internal/app/app_stub_linux_test.go @@ -127,8 +127,8 @@ func (s *stubNixOS) Open(name string) (fs.File, error) { func (s *stubNixOS) Paths() hst.Paths { return hst.Paths{ - SharePath: "/tmp/hakurei.1971", - RuntimePath: "/run/user/1971", - RunDirPath: "/run/user/1971/hakurei", + SharePath: m("/tmp/hakurei.1971"), + RuntimePath: m("/run/user/1971"), + RunDirPath: m("/run/user/1971/hakurei"), } } diff --git a/internal/app/container_linux.go b/internal/app/container_linux.go index 1ea23d4..f93cfcc 100644 --- a/internal/app/container_linux.go +++ b/internal/app/container_linux.go @@ -12,6 +12,7 @@ import ( "hakurei.app/container" "hakurei.app/container/seccomp" "hakurei.app/hst" + "hakurei.app/internal/hlog" "hakurei.app/internal/sys" "hakurei.app/system/dbus" ) @@ -24,7 +25,7 @@ const preallocateOpsCount = 1 << 5 // Note that remaining container setup must be queued by the caller. func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid *int) (*container.Params, map[string]string, error) { if s == nil { - return nil, nil, syscall.EBADE + return nil, nil, hlog.WrapErr(syscall.EBADE, "invalid container configuration") } params := &container.Params{ @@ -73,21 +74,18 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid *gid = container.OverflowGid() } - if s.AutoRoot != "" { - if !path.IsAbs(s.AutoRoot) { - return nil, nil, fmt.Errorf("auto root target %q not absolute", s.AutoRoot) - } + if s.AutoRoot != nil { params.Root(s.AutoRoot, prefix, s.RootFlags) } params. - Proc(container.FHSProc). - Tmpfs(hst.Tmp, 1<<12, 0755) + Proc(container.AbsFHSProc). + Tmpfs(hst.AbsTmp, 1<<12, 0755) if !s.Device { - params.DevWritable(container.FHSDev, true) + params.DevWritable(container.AbsFHSDev, true) } else { - params.Bind(container.FHSDev, container.FHSDev, container.BindWritable|container.BindDevice) + params.Bind(container.AbsFHSDev, container.AbsFHSDev, container.BindWritable|container.BindDevice) } /* retrieve paths and hide them if they're made available in the sandbox; @@ -96,7 +94,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid and should not be treated as such, ALWAYS be careful with what you bind */ var hidePaths []string sc := os.Paths() - hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath) + 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 @@ -132,15 +130,15 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid hidePathSource := make([][2]string, 0, len(s.Filesystem)) // AutoRoot is a collection of many BindMountOp internally - if s.AutoRoot != "" { - if d, err := os.ReadDir(s.AutoRoot); err != nil { + if s.AutoRoot != nil { + if d, err := os.ReadDir(s.AutoRoot.String()); err != nil { return nil, nil, err } else { hidePathSource = slices.Grow(hidePathSource, len(d)) for _, ent := range d { name := ent.Name() if container.IsAutoRootBindable(name) { - name = path.Join(s.AutoRoot, name) + name = path.Join(s.AutoRoot.String(), name) srcP := [2]string{name, name} if err = evalSymlinks(os, &srcP[0]); err != nil { return nil, nil, err @@ -151,16 +149,16 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid } } - for _, c := range s.Filesystem { - if c == nil { - continue + for i, c := range s.Filesystem { + if c.Src == nil { + return nil, nil, fmt.Errorf("invalid filesystem at index %d", i) } // special filesystems - switch c.Src { - case hst.SourceTmpfs: - if !path.IsAbs(c.Dst) { - return nil, nil, fmt.Errorf("tmpfs dst %q is not absolute", c.Dst) + switch c.Src.String() { + case container.Nonexistent: + if c.Dst == nil { + return nil, nil, errors.New("tmpfs dst must not be nil") } if c.Write { params.Tmpfs(c.Dst, hst.TmpfsSize, hst.TmpfsPerm) @@ -170,18 +168,12 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid continue } - if !path.IsAbs(c.Src) { - return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src) + dst := c.Dst + if dst == nil { + dst = c.Src } - dest := c.Dst - if c.Dst == "" { - dest = c.Src - } else if !path.IsAbs(dest) { - return nil, nil, fmt.Errorf("dst path %q is not absolute", dest) - } - - p := [2]string{c.Src, c.Src} + p := [2]string{c.Src.String(), c.Src.String()} if err := evalSymlinks(os, &p[0]); err != nil { return nil, nil, err } @@ -197,7 +189,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid if !c.Must { flags |= container.BindOptional } - params.Bind(c.Src, dest, flags) + params.Bind(c.Src, dst, flags) } for _, p := range hidePathSource { @@ -219,29 +211,49 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid // cover matched paths for i, ok := range hidePathMatch { if ok { - params.Tmpfs(hidePaths[i], 1<<13, 0755) + 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) + } } } - for _, l := range s.Link { - params.Link(l[0], l[1]) + for i, l := range s.Link { + if l.Target == nil || l.Linkname == "" { + return nil, nil, fmt.Errorf("invalid link at index %d", i) + } + linkname := l.Linkname + var dereference bool + if linkname[0] == '*' && path.IsAbs(linkname[1:]) { + linkname = linkname[1:] + dereference = true + } + params.Link(l.Target, linkname, dereference) } if !s.AutoEtc { - if s.Etc != "" { - params.Bind(s.Etc, container.FHSEtc, 0) + if s.Etc != nil { + params.Bind(s.Etc, container.AbsFHSEtc, 0) } } else { - etcPath := s.Etc - if etcPath == "" { - etcPath = container.FHSEtc + if s.Etc == nil { + params.Etc(container.AbsFHSEtc, prefix) + } else { + params.Etc(s.Etc, prefix) } - params.Etc(etcPath, prefix) } // no more ContainerConfig paths beyond this point if !s.Device { - params.Remount(container.FHSDev, syscall.MS_RDONLY) + params.Remount(container.AbsFHSDev, syscall.MS_RDONLY) } return params, maps.Clone(s.Env), nil diff --git a/internal/app/process_linux.go b/internal/app/process_linux.go index 088deab..11d4c8e 100644 --- a/internal/app/process_linux.go +++ b/internal/app/process_linux.go @@ -39,7 +39,7 @@ func (seal *outcome) Run(rs *RunState) error { if err := seal.sys.Commit(seal.ctx); err != nil { return err } - store := state.NewMulti(seal.runDirPath) + store := state.NewMulti(seal.runDirPath.String()) deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store defer func() { var revertErr error @@ -128,7 +128,7 @@ func (seal *outcome) Run(rs *RunState) error { os.Getpid(), seal.waitDelay, seal.container, - seal.user.data, + seal.user.data.String(), hlog.Load(), }) }() diff --git a/internal/app/seal_linux.go b/internal/app/seal_linux.go index 5b2b2c0..e27fee1 100644 --- a/internal/app/seal_linux.go +++ b/internal/app/seal_linux.go @@ -49,10 +49,8 @@ const ( ) var ( - ErrConfig = errors.New("no configuration to seal") - ErrUser = errors.New("invalid aid") - ErrHome = errors.New("invalid home directory") - ErrName = errors.New("invalid username") + ErrIdent = errors.New("invalid identity") + ErrName = errors.New("invalid username") ErrXDisplay = errors.New(display + " unset") @@ -67,8 +65,8 @@ var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z type outcome struct { // copied from initialising [app] id *stringPair[state.ID] - // copied from [sys.State] response - runDirPath string + // 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 @@ -93,9 +91,9 @@ type shareHost struct { // whether XDG_RUNTIME_DIR is used post hsu useRuntimeDir bool // process-specific directory in tmpdir, empty if unused - sharePath string + sharePath *container.Absolute // process-specific directory in XDG_RUNTIME_DIR, empty if unused - runtimeSharePath string + runtimeSharePath *container.Absolute seal *outcome sc hst.Paths @@ -107,48 +105,48 @@ func (share *shareHost) ensureRuntimeDir() { return } share.useRuntimeDir = true - share.seal.sys.Ensure(share.sc.RunDirPath, 0700) - share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath, acl.Execute) - share.seal.sys.Ensure(share.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset - share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath, acl.Execute) + 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() string { - if share.sharePath != "" { +func (share *shareHost) instance() *container.Absolute { + if share.sharePath != nil { return share.sharePath } - share.sharePath = path.Join(share.sc.SharePath, share.seal.id.String()) - share.seal.sys.Ephemeral(system.Process, share.sharePath, 0711) + 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() string { - if share.runtimeSharePath != "" { +func (share *shareHost) runtime() *container.Absolute { + if share.runtimeSharePath != nil { return share.runtimeSharePath } share.ensureRuntimeDir() - share.runtimeSharePath = path.Join(share.sc.RunDirPath, share.seal.id.String()) - share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath, 0700) - share.seal.sys.UpdatePerm(share.runtimeSharePath, acl.Execute) + 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 { - // application id + // identity aid *stringPair[int] - // target uid resolved by fid:aid + // target uid resolved by hid:aid uid *stringPair[int] // supplementary group ids supp []string // home directory host path - data string + data *container.Absolute // app user home directory - home string + home *container.Absolute // passwd database username username string } @@ -159,6 +157,13 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } seal.ctx = ctx + if config == nil { + return hlog.WrapErr(syscall.EINVAL, syscall.EINVAL.Error()) + } + if config.Data == nil { + return hlog.WrapErr(os.ErrInvalid, "invalid data directory") + } + { // encode initial configuration for state tracking ct := new(bytes.Buffer) @@ -171,7 +176,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co // allowed aid range 0 to 9999, this is checked again in hsu if config.Identity < 0 || config.Identity > 9999 { - return hlog.WrapErr(ErrUser, + return hlog.WrapErr(ErrIdent, fmt.Sprintf("identity %d out of range", config.Identity)) } @@ -188,11 +193,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co return hlog.WrapErr(ErrName, fmt.Sprintf("invalid user name %q", seal.user.username)) } - if seal.user.data == "" || !path.IsAbs(seal.user.data) { - return hlog.WrapErr(ErrHome, - fmt.Sprintf("invalid home directory %q", seal.user.data)) - } - if seal.user.home == "" { + if seal.user.home == nil { seal.user.home = seal.user.data } if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil { @@ -210,26 +211,25 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } } - // this also falls back to host path if encountering an invalid path - if !path.IsAbs(config.Shell) { - config.Shell = "/bin/sh" - if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) { - config.Shell = s - } - } - // do not use the value of shell before this point - // 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, _ := sys.LookupEnv(shell) + if a, err := container.NewAbs(s); err == nil { + config.Shell = a + } + } + // hsu clears the environment so resolve paths early - if !path.IsAbs(config.Path) { + if config.Path == nil { if len(config.Args) > 0 { if p, err := sys.LookPath(config.Args[0]); err != nil { return hlog.WrapErr(err, err.Error()) - } else { - config.Path = p + } else if config.Path, err = container.NewAbs(p); err != nil { + return hlog.WrapErr(err, err.Error()) } } else { config.Path = config.Shell @@ -242,26 +242,34 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co Tty: true, AutoEtc: true, - AutoRoot: container.FHSRoot, + AutoRoot: container.AbsFHSRoot, RootFlags: container.BindWritable, } // bind GPU stuff if config.Enablements&(system.EX11|system.EWayland) != 0 { - conf.Filesystem = append(conf.Filesystem, &hst.FilesystemConfig{Src: container.FHSDev + "dri", Device: true}) + conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfig{Src: container.AbsFHSDev.Append("dri"), Device: true}) } // opportunistically bind kvm - conf.Filesystem = append(conf.Filesystem, &hst.FilesystemConfig{Src: container.FHSDev + "kvm", Device: true}) + conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfig{Src: container.AbsFHSDev.Append("kvm"), Device: true}) // hide nscd from container if present - const nscd = container.FHSVar + "run/nscd" - if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) { - conf.Filesystem = append(conf.Filesystem, &hst.FilesystemConfig{Dst: nscd, Src: hst.SourceTmpfs}) + nscd := container.AbsFHSVar.Append("run/nscd") + if _, err := sys.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) { + conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfig{Dst: nscd, Src: container.AbsNonexistent}) } config.Container = conf } + // late nil checks for pd behaviour + if config.Shell == nil { + return hlog.WrapErr(syscall.EINVAL, "invalid shell path") + } + if config.Path == nil { + return hlog.WrapErr(syscall.EINVAL, "invalid program path") + } + var mapuid, mapgid *stringPair[int] { var uid, gid int @@ -272,12 +280,8 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co return hlog.WrapErrSuffix(err, "cannot initialise container configuration:") } - if !path.IsAbs(config.Path) { - return hlog.WrapErr(syscall.EINVAL, - "invalid program path") - } if len(config.Args) == 0 { - config.Args = []string{config.Path} + config.Args = []string{config.Path.String()} } seal.container.Path = config.Path seal.container.Args = config.Args @@ -290,56 +294,52 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } // inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid - innerRuntimeDir := path.Join(container.FHSRunUser, mapuid.String()) - seal.env[xdgRuntimeDir] = innerRuntimeDir + innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String()) + seal.env[xdgRuntimeDir] = innerRuntimeDir.String() seal.env[xdgSessionClass] = "user" seal.env[xdgSessionType] = "tty" share := &shareHost{seal: seal, sc: sys.Paths()} seal.runDirPath = share.sc.RunDirPath seal.sys = system.New(seal.user.uid.unwrap()) - seal.sys.Ensure(share.sc.SharePath, 0711) + seal.sys.Ensure(share.sc.SharePath.String(), 0711) { - runtimeDir := path.Join(share.sc.SharePath, "runtime") - seal.sys.Ensure(runtimeDir, 0700) - seal.sys.UpdatePermType(system.User, runtimeDir, acl.Execute) - runtimeDirInst := path.Join(runtimeDir, seal.user.aid.String()) - seal.sys.Ensure(runtimeDirInst, 0700) - seal.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute) - seal.container.Tmpfs(container.FHSRunUser, 1<<12, 0755) + runtimeDir := share.sc.SharePath.Append("runtime") + seal.sys.Ensure(runtimeDir.String(), 0700) + seal.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute) + runtimeDirInst := runtimeDir.Append(seal.user.aid.String()) + seal.sys.Ensure(runtimeDirInst.String(), 0700) + seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute) + seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755) seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable) } { - tmpdir := path.Join(share.sc.SharePath, "tmpdir") - seal.sys.Ensure(tmpdir, 0700) - seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute) - tmpdirInst := path.Join(tmpdir, seal.user.aid.String()) - seal.sys.Ensure(tmpdirInst, 01700) - seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute) + tmpdir := share.sc.SharePath.Append("tmpdir") + seal.sys.Ensure(tmpdir.String(), 0700) + seal.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute) + tmpdirInst := tmpdir.Append(seal.user.aid.String()) + seal.sys.Ensure(tmpdirInst.String(), 01700) + seal.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute) // mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp - seal.container.Bind(tmpdirInst, container.FHSTmp, container.BindWritable) + seal.container.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable) } { - homeDir := container.FHSVarEmpty - if seal.user.home != "" { - homeDir = seal.user.home - } username := "chronos" if seal.user.username != "" { username = seal.user.username } - seal.container.Bind(seal.user.data, homeDir, container.BindWritable) - seal.container.Dir = homeDir - seal.env["HOME"] = homeDir + seal.container.Bind(seal.user.data, seal.user.home, container.BindWritable) + seal.container.Dir = seal.user.home + seal.env["HOME"] = seal.user.home.String() seal.env["USER"] = username - seal.env[shell] = config.Shell + seal.env[shell] = config.Shell.String() - seal.container.Place(container.FHSEtc+"passwd", - []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+homeDir+":"+config.Shell+"\n")) - seal.container.Place(container.FHSEtc+"group", + seal.container.Place(container.AbsFHSEtc.Append("passwd"), + []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+seal.user.home.String()+":"+config.Shell.String()+"\n")) + seal.container.Place(container.AbsFHSEtc.Append("group"), []byte("hakurei:x:"+mapgid.String()+":\n")) } @@ -350,17 +350,17 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if config.Enablements&system.EWayland != 0 { // outer wayland socket (usually `/run/user/%d/wayland-%d`) - var socketPath string + var socketPath *container.Absolute if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok { hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName) - socketPath = path.Join(share.sc.RuntimePath, wayland.FallbackName) - } else if !path.IsAbs(name) { - socketPath = path.Join(share.sc.RuntimePath, name) + socketPath = share.sc.RuntimePath.Append(wayland.FallbackName) + } else if a, err := container.NewAbs(name); err != nil { + socketPath = share.sc.RuntimePath.Append(name) } else { - socketPath = name + socketPath = a } - innerPath := path.Join(innerRuntimeDir, wayland.FallbackName) + innerPath := innerRuntimeDir.Append(wayland.FallbackName) seal.env[wayland.WaylandDisplay] = wayland.FallbackName if !config.DirectWayland { // set up security-context-v1 @@ -370,14 +370,14 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co appID = "app.hakurei." + seal.id.String() } // downstream socket paths - outerPath := path.Join(share.instance(), "wayland") - seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String()) + outerPath := share.instance().Append("wayland") + seal.sys.Wayland(&seal.sync, outerPath.String(), socketPath.String(), appID, seal.id.String()) seal.container.Bind(outerPath, innerPath, 0) } else { // bind mount wayland socket (insecure) hlog.Verbose("direct wayland access, PROCEED WITH CAUTION") share.ensureRuntimeDir() seal.container.Bind(socketPath, innerPath, 0) - seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute) + seal.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute) } } @@ -388,17 +388,18 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } else { seal.sys.ChangeHosts("#" + seal.user.uid.String()) seal.env[display] = d - seal.container.Bind(container.FHSTmp+".X11-unix", container.FHSTmp+".X11-unix", 0) + socketDir := container.AbsFHSTmp.Append(".X11-unix") + seal.container.Bind(socketDir, socketDir, 0) } } if config.Enablements&system.EPulse != 0 { // PulseAudio runtime directory (usually `/run/user/%d/pulse`) - pulseRuntimeDir := path.Join(share.sc.RuntimePath, "pulse") + pulseRuntimeDir := share.sc.RuntimePath.Append("pulse") // PulseAudio socket (usually `/run/user/%d/pulse/native`) - pulseSocket := path.Join(pulseRuntimeDir, "native") + pulseSocket := pulseRuntimeDir.Append("native") - if _, err := sys.Stat(pulseRuntimeDir); err != nil { + if _, err := sys.Stat(pulseRuntimeDir.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { return hlog.WrapErrSuffix(err, fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir)) @@ -407,7 +408,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir)) } - if s, err := sys.Stat(pulseSocket); err != nil { + if s, err := sys.Stat(pulseSocket.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { return hlog.WrapErrSuffix(err, fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket)) @@ -422,19 +423,19 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } // hard link pulse socket into target-executable share - innerPulseRuntimeDir := path.Join(share.runtime(), "pulse") - innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native") - seal.sys.Link(pulseSocket, innerPulseRuntimeDir) + innerPulseRuntimeDir := share.runtime().Append("pulse") + innerPulseSocket := innerRuntimeDir.Append("pulse", "native") + seal.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String()) seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) - seal.env[pulseServer] = "unix:" + innerPulseSocket + seal.env[pulseServer] = "unix:" + innerPulseSocket.String() // publish current user's pulse cookie for target user if src, err := discoverPulseCookie(sys); err != nil { // not fatal hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message())) } else { - innerDst := hst.Tmp + "/pulse-cookie" - seal.env[pulseCookie] = innerDst + innerDst := hst.AbsTmp.Append("/pulse-cookie") + seal.env[pulseCookie] = innerDst.String() var payload *[]byte seal.container.PlaceP(innerDst, &payload) seal.sys.CopyFile(payload, src, 256, 256) @@ -448,13 +449,12 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } // downstream socket paths - sharePath := share.instance() - sessionPath, systemPath := path.Join(sharePath, "bus"), path.Join(sharePath, "system_bus_socket") + sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket") // configure dbus proxy if f, err := seal.sys.ProxyDBus( config.SessionBus, config.SystemBus, - sessionPath, systemPath, + sessionPath.String(), systemPath.String(), ); err != nil { return err } else { @@ -462,20 +462,20 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } // share proxy sockets - sessionInner := path.Join(innerRuntimeDir, "bus") - seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner + sessionInner := innerRuntimeDir.Append("bus") + seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String() seal.container.Bind(sessionPath, sessionInner, 0) - seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write) + seal.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write) if config.SystemBus != nil { - systemInner := container.FHSRun + "dbus/system_bus_socket" - seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner + systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket") + seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String() seal.container.Bind(systemPath, systemInner, 0) - seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write) + seal.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write) } } // mount root read-only as the final setup Op - seal.container.Remount(container.FHSRoot, syscall.MS_RDONLY) + seal.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY) // append ExtraPerms last for _, p := range config.ExtraPerms { @@ -484,7 +484,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } if p.Ensure { - seal.sys.Ensure(p.Path, 0700) + seal.sys.Ensure(p.Path.String(), 0700) } perms := make(acl.Perms, 0, 3) @@ -497,7 +497,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if p.Execute { perms = append(perms, acl.Execute) } - seal.sys.UpdatePermType(system.User, p.Path, perms...) + seal.sys.UpdatePermType(system.User, p.Path.String(), perms...) } // flatten and sort env for deterministic behaviour diff --git a/internal/sys/interface.go b/internal/sys/interface.go index 04bfe13..5dbe18e 100644 --- a/internal/sys/interface.go +++ b/internal/sys/interface.go @@ -3,10 +3,11 @@ package sys import ( "io/fs" + "log" "os/user" - "path" "strconv" + "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal/hlog" ) @@ -50,18 +51,23 @@ type State interface { // CopyPaths is a generic implementation of [hst.Paths]. func CopyPaths(os State, v *hst.Paths) { - v.SharePath = path.Join(os.TempDir(), "hakurei."+strconv.Itoa(os.Getuid())) - - hlog.Verbosef("process share directory at %q", v.SharePath) - - if r, ok := os.LookupEnv(xdgRuntimeDir); !ok || r == "" || !path.IsAbs(r) { - // fall back to path in share since hakurei has no hard XDG dependency - v.RunDirPath = path.Join(v.SharePath, "run") - v.RuntimePath = path.Join(v.RunDirPath, "compat") + if tempDir, err := container.NewAbs(os.TempDir()); err != nil { + log.Fatalf("invalid TMPDIR: %v", err) } else { - v.RuntimePath = r - v.RunDirPath = path.Join(v.RuntimePath, "hakurei") + v.TempDir = tempDir } + v.SharePath = v.TempDir.Append("hakurei." + strconv.Itoa(os.Getuid())) + hlog.Verbosef("process share directory at %q", v.SharePath) + + r, _ := os.LookupEnv(xdgRuntimeDir) + if a, err := container.NewAbs(r); err != nil { + // fall back to path in share since hakurei has no hard XDG dependency + v.RunDirPath = v.SharePath.Append("run") + v.RuntimePath = v.RunDirPath.Append("compat") + } else { + v.RuntimePath = a + v.RunDirPath = v.RuntimePath.Append("hakurei") + } hlog.Verbosef("runtime directory at %q", v.RunDirPath) } diff --git a/ldd/exec.go b/ldd/exec.go index cb0fec7..79083d4 100644 --- a/ldd/exec.go +++ b/ldd/exec.go @@ -33,14 +33,17 @@ func Exec(ctx context.Context, p string) ([]*Entry, error) { return nil, err } - z := container.NewCommand(c, toolPath.String(), lddName, p) + z := container.NewCommand(c, toolPath, lddName, p) z.Hostname = "hakurei-" + lddName z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompPresets |= seccomp.PresetStrict stdout, stderr := new(bytes.Buffer), new(bytes.Buffer) z.Stdout = stdout z.Stderr = stderr - z.Bind(container.FHSRoot, container.FHSRoot, 0).Proc(container.FHSProc).Dev(container.FHSProc, false) + z. + Bind(container.AbsFHSRoot, container.AbsFHSRoot, 0). + Proc(container.AbsFHSProc). + Dev(container.AbsFHSDev, false) if err := z.Start(); err != nil { return nil, err diff --git a/ldd/path.go b/ldd/path.go index 4e41ce7..a0cdb12 100644 --- a/ldd/path.go +++ b/ldd/path.go @@ -1,21 +1,20 @@ package ldd import ( - "path" - "slices" + "hakurei.app/container" ) // Path returns a deterministic, deduplicated slice of absolute directory paths in entries. -func Path(entries []*Entry) []string { - p := make([]string, 0, len(entries)*2) +func Path(entries []*Entry) []*container.Absolute { + p := make([]*container.Absolute, 0, len(entries)*2) for _, entry := range entries { - if path.IsAbs(entry.Path) { - p = append(p, path.Dir(entry.Path)) + if a, err := container.NewAbs(entry.Path); err == nil { + p = append(p, a.Dir()) } - if path.IsAbs(entry.Name) { - p = append(p, path.Dir(entry.Name)) + if a, err := container.NewAbs(entry.Name); err == nil { + p = append(p, a.Dir()) } } - slices.Sort(p) - return slices.Compact(p) + container.SortAbs(p) + return container.CompactAbs(p) } diff --git a/nixos.nix b/nixos.nix index d67d348..a26b365 100644 --- a/nixos.nix +++ b/nixos.nix @@ -124,6 +124,7 @@ in username = getsubname fid app.identity; data = getsubhome fid app.identity; + inherit (cfg) shell; inherit (app) identity groups; container = { @@ -177,23 +178,23 @@ in auto_etc = true; symlink = [ - [ - "*/run/current-system" - "/run/current-system" - ] + { + target = "/run/current-system"; + linkname = "*/run/current-system"; + } ] ++ optionals (isGraphical && config.hardware.graphics.enable) ( [ - [ - config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument - "/run/opengl-driver" - ] + { + target = "/run/opengl-driver"; + linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument; + } ] ++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [ - [ - config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument - /run/opengl-driver-32 - ] + { + target = "/run/opengl-driver-32"; + linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument; + } ] ); }; diff --git a/options.nix b/options.nix index 90ad356..c6772f6 100644 --- a/options.nix +++ b/options.nix @@ -299,6 +299,14 @@ in ''; }; + shell = mkOption { + type = types.str; + default = "/run/current-system/sw/bin/bash"; + description = '' + Absolute path to preferred shell. + ''; + }; + stateDir = mkOption { type = types.str; description = '' diff --git a/system/dbus/proc.go b/system/dbus/proc.go index d6f9890..0e5f1ce 100644 --- a/system/dbus/proc.go +++ b/system/dbus/proc.go @@ -5,8 +5,6 @@ import ( "errors" "os" "os/exec" - "path" - "slices" "strconv" "syscall" @@ -53,7 +51,7 @@ func (p *Proxy) Start() error { toolPath = a } - var libPaths []string + var libPaths []*container.Absolute if entries, err := ldd.Exec(ctx, toolPath.String()); err != nil { return err } else { @@ -77,42 +75,46 @@ func (p *Proxy) Start() error { } // upstream bus directories - upstreamPaths := make([]string, 0, 2) + upstreamPaths := make([]*container.Absolute, 0, 2) for _, addr := range [][]AddrEntry{p.final.SessionUpstream, p.final.SystemUpstream} { for _, ent := range addr { if ent.Method != "unix" { continue } for _, pair := range ent.Values { - if pair[0] != "path" || !path.IsAbs(pair[1]) { + if pair[0] != "path" { continue } - upstreamPaths = append(upstreamPaths, path.Dir(pair[1])) + if a, err := container.NewAbs(pair[1]); err != nil { + continue + } else { + upstreamPaths = append(upstreamPaths, a.Dir()) + } } } } - slices.Sort(upstreamPaths) - upstreamPaths = slices.Compact(upstreamPaths) + container.SortAbs(upstreamPaths) + upstreamPaths = container.CompactAbs(upstreamPaths) for _, name := range upstreamPaths { z.Bind(name, name, 0) } // parent directories of bind paths - sockDirPaths := make([]string, 0, 2) - if d := path.Dir(p.final.Session[1]); path.IsAbs(d) { - sockDirPaths = append(sockDirPaths, d) + sockDirPaths := make([]*container.Absolute, 0, 2) + if a, err := container.NewAbs(p.final.Session[1]); err == nil { + sockDirPaths = append(sockDirPaths, a.Dir()) } - if d := path.Dir(p.final.System[1]); path.IsAbs(d) { - sockDirPaths = append(sockDirPaths, d) + if a, err := container.NewAbs(p.final.System[1]); err == nil { + sockDirPaths = append(sockDirPaths, a.Dir()) } - slices.Sort(sockDirPaths) - sockDirPaths = slices.Compact(sockDirPaths) + container.SortAbs(sockDirPaths) + sockDirPaths = container.CompactAbs(sockDirPaths) for _, name := range sockDirPaths { z.Bind(name, name, container.BindWritable) } // xdg-dbus-proxy bin path - binPath := path.Dir(toolPath.String()) + binPath := toolPath.Dir() z.Bind(binPath, binPath, 0) }, nil) }