container: use absolute for pathname
Some checks failed
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m20s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 4m54s
Test / Flake checks (push) Has been cancelled
Test / Hpkg (push) Has been cancelled

This is simultaneously more efficient and less error-prone. This change caused minor API changes in multiple other packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-08-11 02:52:32 +09:00
parent 41ac2be965
commit 0de8b76520
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
37 changed files with 832 additions and 709 deletions

View File

@ -115,9 +115,15 @@ func buildCommand(out io.Writer) command.Command {
config.Identity = aid config.Identity = aid
config.Groups = groups config.Groups = groups
config.Data = homeDir
config.Username = userName config.Username = userName
if a, err := container.NewAbs(homeDir); err != nil {
log.Fatal(err.Error())
return err
} else {
config.Data = a
}
if wayland { if wayland {
config.Enablements |= system.EWayland config.Enablements |= system.EWayland
} }
@ -213,7 +219,7 @@ func buildCommand(out io.Writer) command.Command {
var psFlagShort bool var psFlagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error { 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 return errSuccess
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id") }).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")

View File

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

View File

@ -12,6 +12,7 @@ import (
"text/tabwriter" "text/tabwriter"
"time" "time"
"hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
@ -77,13 +78,13 @@ func printShowInstance(
if len(config.Groups) > 0 { if len(config.Groups) > 0 {
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", ")) 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) t.Printf(" Data:\t%s\n", config.Data)
} }
if config.Container != nil { if config.Container != nil {
container := config.Container params := config.Container
if container.Hostname != "" { if params.Hostname != "" {
t.Printf(" Hostname:\t%s\n", container.Hostname) t.Printf(" Hostname:\t%s\n", params.Hostname)
} }
flags := make([]string, 0, 7) flags := make([]string, 0, 7)
writeFlag := func(name string, value bool) { writeFlag := func(name string, value bool) {
@ -91,30 +92,32 @@ func printShowInstance(
flags = append(flags, name) flags = append(flags, name)
} }
} }
writeFlag("userns", container.Userns) writeFlag("userns", params.Userns)
writeFlag("devel", container.Devel) writeFlag("devel", params.Devel)
writeFlag("net", container.Net) writeFlag("net", params.Net)
writeFlag("device", container.Device) writeFlag("device", params.Device)
writeFlag("tty", container.Tty) writeFlag("tty", params.Tty)
writeFlag("mapuid", container.MapRealUID) writeFlag("mapuid", params.MapRealUID)
writeFlag("directwl", config.DirectWayland) writeFlag("directwl", config.DirectWayland)
writeFlag("autoetc", container.AutoEtc) writeFlag("autoetc", params.AutoEtc)
if len(flags) == 0 { if len(flags) == 0 {
flags = append(flags, "none") flags = append(flags, "none")
} }
t.Printf(" Flags:\t%s\n", strings.Join(flags, " ")) t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
if container.AutoRoot != "" { if params.AutoRoot != nil {
t.Printf(" Root:\t%s (%d)\n", container.AutoRoot, container.RootFlags) t.Printf(" Root:\t%s (%d)\n", params.AutoRoot, params.RootFlags)
} }
etc := container.Etc etc := params.Etc
if etc == "" { if etc == nil {
etc = "/etc" etc = container.AbsFHSEtc
} }
t.Printf(" Etc:\t%s\n", etc) 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 { if len(config.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " ")) 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 { if config.Container != nil && len(config.Container.Filesystem) > 0 {
t.Printf("Filesystem\n") t.Printf("Filesystem\n")
for _, f := range config.Container.Filesystem { for _, f := range config.Container.Filesystem {
if f == nil { g := 4
if f.Src == nil {
t.Println(" <invalid>")
continue continue
} else {
g += len(f.Src.String())
}
if f.Dst != nil {
g += len(f.Dst.String())
} }
expr := new(strings.Builder) expr := new(strings.Builder)
expr.Grow(3 + len(f.Src) + 1 + len(f.Dst)) expr.Grow(g)
if f.Device { if f.Device {
expr.WriteString(" d") expr.WriteString(" d")
@ -144,9 +154,14 @@ func printShowInstance(
} else { } else {
expr.WriteString("+") expr.WriteString("+")
} }
expr.WriteString(f.Src) src := f.Src.String()
if f.Dst != "" { if src != container.Nonexistent {
expr.WriteString(":" + f.Dst) expr.WriteString(src)
} else {
expr.WriteString("tmpfs")
}
if f.Dst != nil {
expr.WriteString(":" + f.Dst.String())
} }
t.Printf("%s\n", expr.String()) t.Printf("%s\n", expr.String())
} }

View File

@ -83,18 +83,17 @@ App
Identity: 0 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
Flags: none Flags: none
Etc: /etc Etc: /etc/
Path:
`}, `},
{"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 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
Flags: none Flags: none
Etc: /etc Etc: /etc/
Path:
Filesystem Filesystem
<invalid>
Extra ACL Extra ACL
@ -277,7 +276,7 @@ App
"filesystem": [ "filesystem": [
{ {
"dst": "/tmp/", "dst": "/tmp/",
"src": "tmpfs", "src": "/proc/nonexistent",
"write": true "write": true
}, },
{ {
@ -304,10 +303,10 @@ App
} }
], ],
"symlink": [ "symlink": [
[ {
"/run/user/65534", "target": "/run/user/65534",
"/run/user/150" "linkname": "/run/user/150"
] }
], ],
"auto_root": "/var/lib/hakurei/base/org.debian", "auto_root": "/var/lib/hakurei/base/org.debian",
"root_flags": 2, "root_flags": 2,
@ -409,7 +408,7 @@ App
"filesystem": [ "filesystem": [
{ {
"dst": "/tmp/", "dst": "/tmp/",
"src": "tmpfs", "src": "/proc/nonexistent",
"write": true "write": true
}, },
{ {
@ -436,10 +435,10 @@ App
} }
], ],
"symlink": [ "symlink": [
[ {
"/run/user/65534", "target": "/run/user/65534",
"/run/user/150" "linkname": "/run/user/150"
] }
], ],
"auto_root": "/var/lib/hakurei/base/org.debian", "auto_root": "/var/lib/hakurei/base/org.debian",
"root_flags": 2, "root_flags": 2,
@ -595,7 +594,7 @@ func Test_printPs(t *testing.T) {
"filesystem": [ "filesystem": [
{ {
"dst": "/tmp/", "dst": "/tmp/",
"src": "tmpfs", "src": "/proc/nonexistent",
"write": true "write": true
}, },
{ {
@ -622,10 +621,10 @@ func Test_printPs(t *testing.T) {
} }
], ],
"symlink": [ "symlink": [
[ {
"/run/user/65534", "target": "/run/user/65534",
"/run/user/150" "linkname": "/run/user/150"
] }
], ],
"auto_root": "/var/lib/hakurei/base/org.debian", "auto_root": "/var/lib/hakurei/base/org.debian",
"root_flags": 2, "root_flags": 2,

View File

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"os" "os"
"path"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
@ -56,18 +55,18 @@ type appInfo struct {
// store path to nixGL source // store path to nixGL source
NixGL string `json:"nix_gl,omitempty"` NixGL string `json:"nix_gl,omitempty"`
// store path to activate-and-exec script // store path to activate-and-exec script
Launcher string `json:"launcher"` Launcher *container.Absolute `json:"launcher"`
// store path to /run/current-system // store path to /run/current-system
CurrentSystem string `json:"current_system"` CurrentSystem *container.Absolute `json:"current_system"`
// store path to home-manager activation package // store path to home-manager activation package
ActivationPackage string `json:"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{ config := &hst.Config{
ID: app.ID, ID: app.ID,
Path: argv[0], Path: pathname,
Args: argv, Args: argv,
Enablements: app.Enablements, Enablements: app.Enablements,
@ -77,9 +76,9 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
DirectWayland: app.DirectWayland, DirectWayland: app.DirectWayland,
Username: "hakurei", Username: "hakurei",
Shell: shellPath, Shell: pathShell,
Data: pathSet.homeDir, Data: pathSet.homeDir,
Dir: path.Join("/data/data", app.ID), Dir: pathDataData.Append(app.ID),
Identity: app.Identity, Identity: app.Identity,
Groups: app.Groups, Groups: app.Groups,
@ -92,22 +91,22 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
Device: app.Device, Device: app.Device,
Tty: app.Tty || flagDropShell, Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID, MapRealUID: app.MapRealUID,
Filesystem: []*hst.FilesystemConfig{ Filesystem: []hst.FilesystemConfig{
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true}, {Src: pathSet.nixPath.Append("store"), Dst: pathNixStore, Must: true},
{Src: pathSet.metaPath, Dst: path.Join(hst.Tmp, "app"), Must: true}, {Src: pathSet.metaPath, Dst: hst.AbsTmp.Append("app"), Must: true},
{Src: container.FHSEtc + "resolv.conf"}, {Src: container.AbsFHSEtc.Append("resolv.conf")},
{Src: container.FHSSys + "block"}, {Src: container.AbsFHSSys.Append("block")},
{Src: container.FHSSys + "bus"}, {Src: container.AbsFHSSys.Append("bus")},
{Src: container.FHSSys + "class"}, {Src: container.AbsFHSSys.Append("class")},
{Src: container.FHSSys + "dev"}, {Src: container.AbsFHSSys.Append("dev")},
{Src: container.FHSSys + "devices"}, {Src: container.AbsFHSSys.Append("devices")},
}, },
Link: [][2]string{ Link: []hst.LinkConfig{
{app.CurrentSystem, container.FHSRun + "current-system"}, {app.CurrentSystem, pathCurrentSystem.String()},
{container.FHSRun + "current-system/sw/bin", "/bin"}, {pathSwBin, "/bin"},
{container.FHSRun + "current-system/sw/bin", container.FHSUsrBin}, {pathSwBin, container.FHSUsrBin},
}, },
Etc: path.Join(pathSet.cacheDir, "etc"), Etc: pathSet.cacheDir.Append("etc"),
AutoEtc: true, AutoEtc: true,
}, },
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []*hst.ExtraPermConfig{
@ -141,6 +140,14 @@ func loadAppInfo(name string, beforeFail func()) *appInfo {
beforeFail() beforeFail()
log.Fatal("application identifier must not be empty") 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 return bundle
} }

View File

@ -17,15 +17,13 @@ import (
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
) )
const shellPath = "/run/current-system/sw/bin/bash"
var ( var (
errSuccess = errors.New("success") errSuccess = errors.New("success")
) )
func init() { func init() {
hlog.Prepare("hpkg") 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) log.Fatalf("cannot set $SHELL: %v", err)
} }
} }
@ -82,31 +80,32 @@ func main() {
Extract package and set up for cleanup. Extract package and set up for cleanup.
*/ */
var workDir string var workDir *container.Absolute
if p, err := os.MkdirTemp("", "hpkg.*"); err != nil { if p, err := os.MkdirTemp("", "hpkg.*"); err != nil {
log.Printf("cannot create temporary directory: %v", err) log.Printf("cannot create temporary directory: %v", err)
return err return err
} else { } else if workDir, err = container.NewAbs(p); err != nil {
workDir = p log.Printf("invalid temporary directory: %v", err)
return err
} }
cleanup := func() { cleanup := func() {
// should be faster than a native implementation // should be faster than a native implementation
mustRun(chmod, "-R", "+w", workDir) mustRun(chmod, "-R", "+w", workDir.String())
mustRun(rm, "-rf", workDir) mustRun(rm, "-rf", workDir.String())
} }
beforeRunFail.Store(&cleanup) 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. 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) pathSet := pathSetByApp(bundle.ID)
a := bundle 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) { if !os.IsNotExist(err) {
cleanup() cleanup()
log.Printf("cannot access %q: %v", pathSet.metaPath, err) 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) log.Printf("metadata path %q is not a file", pathSet.metaPath)
return syscall.EBADMSG return syscall.EBADMSG
} else { } else {
a = loadAppInfo(pathSet.metaPath, cleanup) a = loadAppInfo(pathSet.metaPath.String(), cleanup)
if a.ID != bundle.ID { if a.ID != bundle.ID {
cleanup() cleanup()
log.Printf("app %q claims to have identifier %q", log.Printf("app %q claims to have identifier %q",
@ -209,7 +208,7 @@ func main() {
*/ */
// serialise metadata to ensure consistency // 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() cleanup()
log.Printf("cannot create metadata file: %v", err) log.Printf("cannot create metadata file: %v", err)
return err return err
@ -222,7 +221,7 @@ func main() {
// not fatal // 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() cleanup()
log.Printf("cannot rename metadata file: %v", err) log.Printf("cannot rename metadata file: %v", err)
return err return err
@ -251,7 +250,7 @@ func main() {
id := args[0] id := args[0]
pathSet := pathSetByApp(id) pathSet := pathSetByApp(id)
a := loadAppInfo(pathSet.metaPath, func() {}) a := loadAppInfo(pathSet.metaPath.String(), func() {})
if a.ID != id { if a.ID != id {
log.Printf("app %q claims to have identifier %q", id, a.ID) log.Printf("app %q claims to have identifier %q", id, a.ID)
return syscall.EBADE return syscall.EBADE
@ -275,13 +274,13 @@ func main() {
"--override-input nixpkgs path:/etc/nixpkgs " + "--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + a.NixGL + "#nixVulkanNvidia", "path:" + a.NixGL + "#nixVulkanNvidia",
}, true, func(config *hst.Config) *hst.Config { }, true, func(config *hst.Config) *hst.Config {
config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{ config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfig{
{Src: container.FHSEtc + "resolv.conf"}, {Src: container.AbsFHSEtc.Append("resolv.conf")},
{Src: container.FHSSys + "block"}, {Src: container.AbsFHSSys.Append("block")},
{Src: container.FHSSys + "bus"}, {Src: container.AbsFHSSys.Append("bus")},
{Src: container.FHSSys + "class"}, {Src: container.AbsFHSSys.Append("class")},
{Src: container.FHSSys + "dev"}, {Src: container.AbsFHSSys.Append("dev")},
{Src: container.FHSSys + "devices"}, {Src: container.AbsFHSSys.Append("devices")},
}...) }...)
appendGPUFilesystem(config) appendGPUFilesystem(config)
return config return config
@ -292,15 +291,11 @@ func main() {
Create app configuration. Create app configuration.
*/ */
argv := make([]string, 1, len(args)) pathname := a.Launcher
if !flagDropShell { if flagDropShell {
argv[0] = a.Launcher pathname = pathShell
} else {
argv[0] = shellPath
} }
argv = append(argv, args[1:]...) config := a.toHst(pathSet, pathname, args, flagDropShell)
config := a.toFst(pathSet, argv, flagDropShell)
/* /*
Expose GPU devices. Expose GPU devices.
@ -308,7 +303,7 @@ func main() {
if a.GPU { if a.GPU {
config.Container.Filesystem = append(config.Container.Filesystem, 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) appendGPUFilesystem(config)
} }

View File

@ -4,7 +4,6 @@ import (
"log" "log"
"os" "os"
"os/exec" "os/exec"
"path"
"strconv" "strconv"
"sync/atomic" "sync/atomic"
@ -13,19 +12,32 @@ import (
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
) )
const bash = "bash"
var ( var (
dataHome string dataHome *container.Absolute
) )
func init() { func init() {
// dataHome // dataHome
if p, ok := os.LookupEnv("HAKUREI_DATA_HOME"); ok { if a, err := container.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil {
dataHome = p dataHome = a
} else { } else {
dataHome = container.FHSVarLib + "hakurei/" + strconv.Itoa(os.Getuid()) dataHome = container.AbsFHSVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid()))
} }
} }
var (
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 { func lookPath(file string) string {
if p, err := exec.LookPath(file); err != nil { if p, err := exec.LookPath(file); err != nil {
log.Fatalf("%s: command not found", file) log.Fatalf("%s: command not found", file)
@ -51,52 +63,52 @@ func mustRun(name string, arg ...string) {
type appPathSet struct { type appPathSet struct {
// ${dataHome}/${id} // ${dataHome}/${id}
baseDir string baseDir *container.Absolute
// ${baseDir}/app // ${baseDir}/app
metaPath string metaPath *container.Absolute
// ${baseDir}/files // ${baseDir}/files
homeDir string homeDir *container.Absolute
// ${baseDir}/cache // ${baseDir}/cache
cacheDir string cacheDir *container.Absolute
// ${baseDir}/cache/nix // ${baseDir}/cache/nix
nixPath string nixPath *container.Absolute
} }
func pathSetByApp(id string) *appPathSet { func pathSetByApp(id string) *appPathSet {
pathSet := new(appPathSet) pathSet := new(appPathSet)
pathSet.baseDir = path.Join(dataHome, id) pathSet.baseDir = dataHome.Append(id)
pathSet.metaPath = path.Join(pathSet.baseDir, "app") pathSet.metaPath = pathSet.baseDir.Append("app")
pathSet.homeDir = path.Join(pathSet.baseDir, "files") pathSet.homeDir = pathSet.baseDir.Append("files")
pathSet.cacheDir = path.Join(pathSet.baseDir, "cache") pathSet.cacheDir = pathSet.baseDir.Append("cache")
pathSet.nixPath = path.Join(pathSet.cacheDir, "nix") pathSet.nixPath = pathSet.cacheDir.Append("nix")
return pathSet return pathSet
} }
func appendGPUFilesystem(config *hst.Config) { 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 // flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{Src: "/dev/dri", Device: true}, {Src: container.AbsFHSDev.Append("dri"), Device: true},
// mali // mali
{Src: "/dev/mali", Device: true}, {Src: container.AbsFHSDev.Append("mali"), Device: true},
{Src: "/dev/mali0", Device: true}, {Src: container.AbsFHSDev.Append("mali0"), Device: true},
{Src: "/dev/umplock", Device: true}, {Src: container.AbsFHSDev.Append("umplock"), Device: true},
// nvidia // nvidia
{Src: "/dev/nvidiactl", Device: true}, {Src: container.AbsFHSDev.Append("nvidiactl"), Device: true},
{Src: "/dev/nvidia-modeset", Device: true}, {Src: container.AbsFHSDev.Append("nvidia-modeset"), Device: true},
// nvidia OpenCL/CUDA // nvidia OpenCL/CUDA
{Src: "/dev/nvidia-uvm", Device: true}, {Src: container.AbsFHSDev.Append("nvidia-uvm"), Device: true},
{Src: "/dev/nvidia-uvm-tools", Device: true}, {Src: container.AbsFHSDev.Append("nvidia-uvm-tools"), Device: true},
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff // flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
{Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true}, {Src: container.AbsFHSDev.Append("nvidia0"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia1"), Device: true},
{Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true}, {Src: container.AbsFHSDev.Append("nvidia2"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia3"), Device: true},
{Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true}, {Src: container.AbsFHSDev.Append("nvidia4"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia5"), Device: true},
{Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true}, {Src: container.AbsFHSDev.Append("nvidia6"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia7"), Device: true},
{Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true}, {Src: container.AbsFHSDev.Append("nvidia8"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia9"), Device: true},
{Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true}, {Src: container.AbsFHSDev.Append("nvidia10"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia11"), Device: true},
{Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true}, {Src: container.AbsFHSDev.Append("nvidia12"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia13"), Device: true},
{Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true}, {Src: container.AbsFHSDev.Append("nvidia14"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia15"), Device: true},
{Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true}, {Src: container.AbsFHSDev.Append("nvidia16"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia17"), Device: true},
{Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true}, {Src: container.AbsFHSDev.Append("nvidia18"), Device: true}, {Src: container.AbsFHSDev.Append("nvidia19"), Device: true},
}...) }...)
} }

View File

@ -2,7 +2,6 @@ package main
import ( import (
"context" "context"
"path"
"strings" "strings"
"hakurei.app/container" "hakurei.app/container"
@ -19,8 +18,8 @@ func withNixDaemon(
mustRunAppDropShell(ctx, updateConfig(&hst.Config{ mustRunAppDropShell(ctx, updateConfig(&hst.Config{
ID: app.ID, ID: app.ID,
Path: shellPath, Path: pathShell,
Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " + Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon // start nix-daemon
"nix-daemon --store / & " + "nix-daemon --store / & " +
// wait for socket to appear // wait for socket to appear
@ -33,9 +32,9 @@ func withNixDaemon(
}, },
Username: "hakurei", Username: "hakurei",
Shell: shellPath, Shell: pathShell,
Data: pathSet.homeDir, Data: pathSet.homeDir,
Dir: path.Join("/data/data", app.ID), Dir: pathDataData.Append(app.ID),
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
@ -49,15 +48,15 @@ func withNixDaemon(
Net: net, Net: net,
SeccompFlags: seccomp.AllowMultiarch, SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell, Tty: dropShell,
Filesystem: []*hst.FilesystemConfig{ Filesystem: []hst.FilesystemConfig{
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true}, {Src: pathSet.nixPath, Dst: pathNix, Write: true, Must: true},
}, },
Link: [][2]string{ Link: []hst.LinkConfig{
{app.CurrentSystem, container.FHSRun + "current-system"}, {app.CurrentSystem, pathCurrentSystem.String()},
{container.FHSRun + "current-system/sw/bin", "/bin"}, {pathSwBin, "/bin"},
{container.FHSRun + "current-system/sw/bin", container.FHSUsrBin}, {pathSwBin, container.FHSUsrBin},
}, },
Etc: path.Join(pathSet.cacheDir, "etc"), Etc: pathSet.cacheDir.Append("etc"),
AutoEtc: true, AutoEtc: true,
}, },
}), dropShell, beforeFail) }), dropShell, beforeFail)
@ -65,18 +64,18 @@ func withNixDaemon(
func withCacheDir( func withCacheDir(
ctx context.Context, ctx context.Context,
action string, command []string, workDir string, action string, command []string, workDir *container.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) { app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &hst.Config{ mustRunAppDropShell(ctx, &hst.Config{
ID: app.ID, ID: app.ID,
Path: shellPath, Path: pathShell,
Args: []string{shellPath, "-lc", strings.Join(command, " && ")}, Args: []string{bash, "-lc", strings.Join(command, " && ")},
Username: "nixos", Username: "nixos",
Shell: shellPath, Shell: pathShell,
Data: pathSet.cacheDir, // this also ensures cacheDir via shim 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{ ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
@ -89,16 +88,16 @@ func withCacheDir(
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
SeccompFlags: seccomp.AllowMultiarch, SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell, Tty: dropShell,
Filesystem: []*hst.FilesystemConfig{ Filesystem: []hst.FilesystemConfig{
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true}, {Src: workDir.Append("nix"), Dst: pathNix, Must: true},
{Src: workDir, Dst: path.Join(hst.Tmp, "bundle"), Must: true}, {Src: workDir, Dst: hst.AbsTmp.Append("bundle"), Must: true},
}, },
Link: [][2]string{ Link: []hst.LinkConfig{
{app.CurrentSystem, container.FHSRun + "current-system"}, {app.CurrentSystem, pathCurrentSystem.String()},
{container.FHSRun + "current-system/sw/bin", "/bin"}, {pathSwBin, "/bin"},
{container.FHSRun + "current-system/sw/bin", container.FHSUsrBin}, {pathSwBin, container.FHSUsrBin},
}, },
Etc: path.Join(workDir, container.FHSEtc), Etc: workDir.Append(container.FHSEtc),
AutoEtc: true, AutoEtc: true,
}, },
}, dropShell, beforeFail) }, dropShell, beforeFail)
@ -106,7 +105,7 @@ func withCacheDir(
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) { func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
if dropShell { if dropShell {
config.Args = []string{shellPath, "-l"} config.Args = []string{bash, "-l"}
mustRunApp(ctx, config, beforeFail) mustRunApp(ctx, config, beforeFail)
beforeFail() beforeFail()
internal.Exit(0) internal.Exit(0)

View File

@ -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. // 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. // 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} e := &AutoEtcOp{prefix}
f.Mkdir(FHSEtc, 0755) f.Mkdir(AbsFHSEtc, 0755)
f.Bind(host, e.hostPath(), 0) f.Bind(host, e.hostPath(), 0)
*f = append(*f, e) *f = append(*f, e)
return f return f
@ -28,7 +28,7 @@ func (e *AutoEtcOp) apply(*Params) error {
if err := os.MkdirAll(target, 0755); err != nil { if err := os.MkdirAll(target, 0755); err != nil {
return wrapErrSelf(err) 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) return wrapErrSelf(err)
} else { } else {
for _, ent := range d { for _, ent := range d {
@ -54,8 +54,10 @@ func (e *AutoEtcOp) apply(*Params) error {
return nil 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 { func (e *AutoEtcOp) Is(op Op) bool {
ve, ok := op.(*AutoEtcOp) ve, ok := op.(*AutoEtcOp)

View File

@ -4,21 +4,21 @@ import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"os" "os"
"path" "syscall"
. "syscall"
) )
func init() { gob.Register(new(AutoRootOp)) } func init() { gob.Register(new(AutoRootOp)) }
// Root appends an [Op] that expands a directory into a toplevel bind mount mirror on container root. // 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. // 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}) *f = append(*f, &AutoRootOp{host, prefix, flags, nil})
return f return f
} }
type AutoRootOp struct { type AutoRootOp struct {
Host, Prefix string Host *Absolute
Prefix string
// passed through to bindMount // passed through to bindMount
Flags int Flags int
@ -29,11 +29,11 @@ type AutoRootOp struct {
} }
func (r *AutoRootOp) early(params *Params) error { func (r *AutoRootOp) early(params *Params) error {
if !path.IsAbs(r.Host) { if r.Host == nil {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", r.Host)) 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) return wrapErrSelf(err)
} else { } else {
r.resolved = make([]Op, 0, len(d)) r.resolved = make([]Op, 0, len(d))
@ -41,8 +41,8 @@ func (r *AutoRootOp) early(params *Params) error {
name := ent.Name() name := ent.Name()
if IsAutoRootBindable(name) { if IsAutoRootBindable(name) {
op := &BindMountOp{ op := &BindMountOp{
Source: path.Join(r.Host, name), Source: r.Host.Append(name),
Target: FHSRoot + name, Target: AbsFHSRoot.Append(name),
Flags: r.Flags, Flags: r.Flags,
} }
if err = op.early(params); err != nil { if err = op.early(params); err != nil {

View File

@ -9,7 +9,6 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path"
"strconv" "strconv"
. "syscall" . "syscall"
"time" "time"
@ -53,11 +52,11 @@ type (
// Params holds container configuration and is safe to serialise. // Params holds container configuration and is safe to serialise.
Params struct { Params struct {
// Working directory in the container. // Working directory in the container.
Dir string Dir *Absolute
// Initial process environment. // Initial process environment.
Env []string Env []string
// Absolute path of initial process in the container. Overrides name. // Pathname of initial process in the container.
Path string Path *Absolute
// Initial process argv. // Initial process argv.
Args []string Args []string
// Deliver SIGINT to the initial process on context cancellation. // Deliver SIGINT to the initial process on context cancellation.
@ -188,14 +187,16 @@ func (p *Container) Serve() error {
setup := p.setup setup := p.setup
p.setup = nil p.setup = nil
if !path.IsAbs(p.Path) { if p.Path == nil {
p.cancel() p.cancel()
return msg.WrapErr(EINVAL, return msg.WrapErr(EINVAL, "invalid executable pathname")
fmt.Sprintf("invalid executable path %q", p.Path))
} }
// do not transmit nil
if p.Dir == nil {
p.Dir = AbsFHSRoot
}
if p.SeccompRules == nil { if p.SeccompRules == nil {
// do not transmit nil
p.SeccompRules = make([]seccomp.NativeRule, 0) 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. // New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context) *Container { func New(ctx context.Context) *Container {
return &Container{ctx: ctx, Params: Params{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. // 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 := New(ctx)
z.Path = pathname z.Path = pathname
z.Args = append([]string{name}, args...) z.Args = append([]string{name}, args...)

View File

@ -10,7 +10,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@ -77,7 +76,7 @@ var containerTestCases = []struct {
{"tmpfs", true, false, false, true, {"tmpfs", true, false, false, true,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
Tmpfs(hst.Tmp, 0, 0755), Tmpfs(hst.AbsTmp, 0, 0755),
), ),
earlyMnt( earlyMnt(
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore), 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, {"dev", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
Dev("/dev", true), Dev(container.MustAbs("/dev"), true),
), ),
earlyMnt( earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), 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, {"dev no mqueue", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
Dev("/dev", false), Dev(container.MustAbs("/dev"), false),
), ),
earlyMnt( earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
@ -119,20 +118,20 @@ var containerTestCases = []struct {
{"overlay", true, false, false, true, {"overlay", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) { func(t *testing.T) (*container.Ops, context.Context) {
tempDir := t.TempDir() tempDir := container.MustAbs(t.TempDir())
lower0, lower1, upper, work := lower0, lower1, upper, work :=
path.Join(tempDir, "lower0"), tempDir.Append("lower0"),
path.Join(tempDir, "lower1"), tempDir.Append("lower1"),
path.Join(tempDir, "upper"), tempDir.Append("upper"),
path.Join(tempDir, "work") tempDir.Append("work")
for _, name := range []string{lower0, lower1, upper, work} { for _, a := range []*container.Absolute{lower0, lower1, upper, work} {
if err := os.Mkdir(name, 0755); err != nil { if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err) t.Fatalf("Mkdir: error = %v", err)
} }
} }
return new(container.Ops). 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(), context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1), testVal("lower1"), lower1),
testVal("lower0"), lower0), testVal("lower0"), lower0),
@ -143,12 +142,12 @@ var containerTestCases = []struct {
return []*vfs.MountInfoEntry{ return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay", ent("/", hst.Tmp, "rw", "overlay", "overlay",
"rw,lowerdir="+ "rw,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(string))+":"+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(string))+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",upperdir="+ ",upperdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(string))+ container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+
",workdir="+ ",workdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(string))+ container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+
",redirect_dir=nofollow,uuid=on,userxattr"), ",redirect_dir=nofollow,uuid=on,userxattr"),
} }
}, },
@ -156,18 +155,18 @@ var containerTestCases = []struct {
{"overlay ephemeral", true, false, false, true, {"overlay ephemeral", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) { func(t *testing.T) (*container.Ops, context.Context) {
tempDir := t.TempDir() tempDir := container.MustAbs(t.TempDir())
lower0, lower1 := lower0, lower1 :=
path.Join(tempDir, "lower0"), tempDir.Append("lower0"),
path.Join(tempDir, "lower1") tempDir.Append("lower1")
for _, name := range []string{lower0, lower1} { for _, a := range []*container.Absolute{lower0, lower1} {
if err := os.Mkdir(name, 0755); err != nil { if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err) t.Fatalf("Mkdir: error = %v", err)
} }
} }
return new(container.Ops). return new(container.Ops).
OverlayEphemeral(hst.Tmp, lower0, lower1), OverlayEphemeral(hst.AbsTmp, lower0, lower1),
t.Context() t.Context()
}, },
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry { func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
@ -180,17 +179,17 @@ var containerTestCases = []struct {
{"overlay readonly", true, false, false, true, {"overlay readonly", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) { func(t *testing.T) (*container.Ops, context.Context) {
tempDir := t.TempDir() tempDir := container.MustAbs(t.TempDir())
lower0, lower1 := lower0, lower1 :=
path.Join(tempDir, "lower0"), tempDir.Append("lower0"),
path.Join(tempDir, "lower1") tempDir.Append("lower1")
for _, name := range []string{lower0, lower1} { for _, a := range []*container.Absolute{lower0, lower1} {
if err := os.Mkdir(name, 0755); err != nil { if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err) t.Fatalf("Mkdir: error = %v", err)
} }
} }
return new(container.Ops). return new(container.Ops).
OverlayReadonly(hst.Tmp, lower0, lower1), OverlayReadonly(hst.AbsTmp, lower0, lower1),
context.WithValue(context.WithValue(t.Context(), context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1), testVal("lower1"), lower1),
testVal("lower0"), lower0) testVal("lower0"), lower0)
@ -199,8 +198,8 @@ var containerTestCases = []struct {
return []*vfs.MountInfoEntry{ return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay", ent("/", hst.Tmp, "rw", "overlay", "overlay",
"ro,lowerdir="+ "ro,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(string))+":"+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(string))+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",redirect_dir=nofollow,userxattr"), ",redirect_dir=nofollow,userxattr"),
} }
}, },
@ -252,7 +251,7 @@ func TestContainer(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel() defer cancel()
var libPaths []string var libPaths []*container.Absolute
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i)) c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid c.Uid = tc.uid
c.Gid = tc.gid c.Gid = tc.gid
@ -273,11 +272,11 @@ func TestContainer(t *testing.T) {
c.HostNet = tc.net c.HostNet = tc.net
c. c.
Readonly(pathReadonly, 0755). Readonly(container.MustAbs(pathReadonly), 0755).
Tmpfs("/tmp", 0, 0755). Tmpfs(container.MustAbs("/tmp"), 0, 0755).
Place("/etc/hostname", []byte(c.Hostname)) Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname))
// needs /proc to check mountinfo // needs /proc to check mountinfo
c.Proc("/proc") c.Proc(container.MustAbs("/proc"))
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism // mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
@ -286,9 +285,9 @@ func TestContainer(t *testing.T) {
// Bind(os.Args[0], helperInnerPath, 0) // Bind(os.Args[0], helperInnerPath, 0)
ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore), ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
) )
for _, name := range libPaths { for _, a := range libPaths {
// Bind(name, name, 0) // 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, wantMnt...)
mnt = append(mnt, mnt = append(mnt,
@ -308,10 +307,10 @@ func TestContainer(t *testing.T) {
_, _ = output.WriteTo(os.Stdout) _, _ = output.WriteTo(os.Stdout)
t.Fatalf("cannot serialise expected mount points: %v", err) t.Fatalf("cannot serialise expected mount points: %v", err)
} }
c.Place(pathWantMnt, want.Bytes()) c.Place(container.MustAbs(pathWantMnt), want.Bytes())
if tc.ro { if tc.ro {
c.Remount("/", syscall.MS_RDONLY) c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
} }
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
@ -392,7 +391,7 @@ func testContainerCancel(
} }
func TestContainerString(t *testing.T) { 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.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset( c.SeccompRules = seccomp.Preset(
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY, seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,

View File

@ -268,12 +268,12 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
Umask(oldmask) 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.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = params.Args cmd.Args = params.Args
cmd.Env = params.Env cmd.Env = params.Env
cmd.ExtraFiles = extraFiles cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir cmd.Dir = params.Dir.String()
msg.Verbosef("starting initial program %s", params.Path) msg.Verbosef("starting initial program %s", params.Path)
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {

View File

@ -21,6 +21,10 @@ const (
helperInnerPath = "/usr/bin/helper" helperInnerPath = "/usr/bin/helper"
) )
var (
absHelperInnerPath = container.MustAbs(helperInnerPath)
)
var helperCommands []func(c command.Command) var helperCommands []func(c command.Command)
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -46,10 +50,10 @@ func TestMain(m *testing.M) {
os.Exit(m.Run()) os.Exit(m.Run())
} }
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]string, args ...string) (c *container.Container) { func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) {
c = container.NewCommand(ctx, helperInnerPath, "helper", args...) c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1") 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 // in case test has cgo enabled
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil { 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) { func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
return helperNewContainerLibPaths(ctx, new([]string), args...) return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...)
} }

View File

@ -47,22 +47,22 @@ func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
func init() { gob.Register(new(RemountOp)) } func init() { gob.Register(new(RemountOp)) }
// Remount appends an [Op] that applies [RemountOp.Flags] on container path [RemountOp.Target]. // 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}) *f = append(*f, &RemountOp{target, flags})
return f return f
} }
type RemountOp struct { type RemountOp struct {
Target string Target *Absolute
Flags uintptr Flags uintptr
} }
func (*RemountOp) early(*Params) error { return nil } func (*RemountOp) early(*Params) error { return nil }
func (r *RemountOp) apply(*Params) error { func (r *RemountOp) apply(*Params) error {
if !path.IsAbs(r.Target) { if r.Target == nil {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", r.Target)) 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)) 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)) } func init() { gob.Register(new(BindMountOp)) }
// Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target]. // 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 { func (f *Ops) Bind(source, target *Absolute, flags int) *Ops {
*f = append(*f, &BindMountOp{source, "", target, flags}) *f = append(*f, &BindMountOp{nil, source, target, flags})
return f return f
} }
type BindMountOp struct { type BindMountOp struct {
Source, sourceFinal, Target string sourceFinal, Source, Target *Absolute
Flags int Flags int
} }
@ -94,24 +94,24 @@ const (
) )
func (b *BindMountOp) early(*Params) error { func (b *BindMountOp) early(*Params) error {
if !path.IsAbs(b.Source) { if b.Source == nil || b.Target == nil {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", b.Source)) 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 { if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
b.sourceFinal = "\x00" // leave sourceFinal as nil
return nil return nil
} }
return wrapErrSelf(err) return wrapErrSelf(err)
} else { } else {
b.sourceFinal = v b.sourceFinal, err = NewAbs(pathname)
return nil return err
} }
} }
func (b *BindMountOp) apply(*Params) error { func (b *BindMountOp) apply(*Params) error {
if b.sourceFinal == "\x00" { if b.sourceFinal == nil {
if b.Flags&BindOptional == 0 { if b.Flags&BindOptional == 0 {
// unreachable // unreachable
return EBADE return EBADE
@ -119,12 +119,8 @@ func (b *BindMountOp) apply(*Params) error {
return nil return nil
} }
if !path.IsAbs(b.sourceFinal) || !path.IsAbs(b.Target) { source := toHost(b.sourceFinal.String())
return msg.WrapErr(EBADE, "path is not absolute") target := toSysroot(b.Target.String())
}
source := toHost(b.sourceFinal)
target := toSysroot(b.Target)
// this perm value emulates bwrap behaviour as it clears bits from 0755 based on // this perm value emulates bwrap behaviour as it clears bits from 0755 based on
// op->perms which is never set for any bind setup op so always results in 0700 // op->perms which is never set for any bind setup op so always results in 0700
@ -161,60 +157,62 @@ func (b *BindMountOp) String() string {
func init() { gob.Register(new(MountProcOp)) } func init() { gob.Register(new(MountProcOp)) }
// Proc appends an [Op] that mounts a private instance of proc. // Proc appends an [Op] that mounts a private instance of proc.
func (f *Ops) Proc(dest string) *Ops { func (f *Ops) Proc(target *Absolute) *Ops {
*f = append(*f, MountProcOp(dest)) *f = append(*f, &MountProcOp{target})
return f return f
} }
type MountProcOp string type MountProcOp struct {
Target *Absolute
}
func (p MountProcOp) early(*Params) error { return nil } func (p *MountProcOp) early(*Params) error { return nil }
func (p MountProcOp) apply(params *Params) error { func (p *MountProcOp) apply(params *Params) error {
v := string(p) if p.Target == nil {
return EBADE
if !path.IsAbs(v) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
} }
target := toSysroot(p.Target.String())
target := toSysroot(v)
if err := os.MkdirAll(target, params.ParentPerm); err != nil { if err := os.MkdirAll(target, params.ParentPerm); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} }
return wrapErrSuffix(Mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString), 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 (p *MountProcOp) Is(op Op) bool {
func (MountProcOp) prefix() string { return "mounting" } vp, ok := op.(*MountProcOp)
func (p MountProcOp) String() string { return fmt.Sprintf("proc on %q", string(p)) } 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)) } func init() { gob.Register(new(MountDevOp)) }
// Dev appends an [Op] that mounts a subset of host /dev. // Dev appends an [Op] that mounts a subset of host /dev.
func (f *Ops) Dev(dest string, mqueue bool) *Ops { func (f *Ops) Dev(target *Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{dest, mqueue, false}) *f = append(*f, &MountDevOp{target, mqueue, false})
return f return f
} }
// DevWritable appends an [Op] that mounts a writable subset of host /dev. // 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]. // 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 { func (f *Ops) DevWritable(target *Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{dest, mqueue, true}) *f = append(*f, &MountDevOp{target, mqueue, true})
return f return f
} }
type MountDevOp struct { type MountDevOp struct {
Target string Target *Absolute
Mqueue bool Mqueue bool
Write bool Write bool
} }
func (d *MountDevOp) early(*Params) error { return nil } func (d *MountDevOp) early(*Params) error { return nil }
func (d *MountDevOp) apply(params *Params) error { func (d *MountDevOp) apply(params *Params) error {
if !path.IsAbs(d.Target) { if d.Target == nil {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", d.Target)) 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 { if err := mountTmpfs(SourceTmpfsDevtmpfs, target, MS_NOSUID|MS_NODEV, 0, params.ParentPerm); err != nil {
return err return err
@ -314,20 +312,20 @@ func (d *MountDevOp) String() string {
func init() { gob.Register(new(MountTmpfsOp)) } func init() { gob.Register(new(MountTmpfsOp)) }
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path]. // Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops { func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, dest, MS_NOSUID | MS_NODEV, size, perm}) *f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm})
return f return f
} }
// Readonly appends an [Op] that mounts read-only tmpfs on container path [MountTmpfsOp.Path]. // Readonly appends an [Op] that mounts read-only tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Readonly(dest string, perm os.FileMode) *Ops { func (f *Ops) Readonly(target *Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, dest, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm}) *f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, target, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm})
return f return f
} }
type MountTmpfsOp struct { type MountTmpfsOp struct {
FSName string FSName string
Path string Path *Absolute
Flags uintptr Flags uintptr
Size int Size int
Perm os.FileMode Perm os.FileMode
@ -335,13 +333,13 @@ type MountTmpfsOp struct {
func (t *MountTmpfsOp) early(*Params) error { return nil } func (t *MountTmpfsOp) early(*Params) error { return nil }
func (t *MountTmpfsOp) apply(*Params) error { func (t *MountTmpfsOp) apply(*Params) error {
if !path.IsAbs(t.Path) { if t.Path == nil {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) return EBADE
} }
if t.Size < 0 || t.Size > math.MaxUint>>1 { if t.Size < 0 || t.Size > math.MaxUint>>1 {
return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size)) 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 } 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)) } func init() { gob.Register(new(MountOverlayOp)) }
// Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]. // Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target].
func (f *Ops) Overlay(target, state, work string, layers ...string) *Ops { func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops {
*f = append(*f, &MountOverlayOp{ *f = append(*f, &MountOverlayOp{
Target: target, Target: target,
Lower: layers, 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] // OverlayEphemeral appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]
// with an ephemeral upperdir and workdir. // with an ephemeral upperdir and workdir.
func (f *Ops) OverlayEphemeral(target string, layers ...string) *Ops { func (f *Ops) OverlayEphemeral(target *Absolute, layers ...*Absolute) *Ops {
return f.Overlay(target, SourceTmpfsEphemeral, zeroString, layers...) return f.Overlay(target, AbsFHSRoot, nil, layers...)
} }
// OverlayReadonly appends an [Op] that mounts the overlay pseudo filesystem readonly on [MountOverlayOp.Target] // OverlayReadonly appends an [Op] that mounts the overlay pseudo filesystem readonly on [MountOverlayOp.Target]
func (f *Ops) OverlayReadonly(target string, layers ...string) *Ops { func (f *Ops) OverlayReadonly(target *Absolute, layers ...*Absolute) *Ops {
return f.Overlay(target, zeroString, zeroString, layers...) return f.Overlay(target, nil, nil, layers...)
} }
type MountOverlayOp struct { type MountOverlayOp struct {
Target string Target *Absolute
// formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early; // Any filesystem, does not need to be on a writable filesystem.
Lower []string Lower []*Absolute
// formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early; // 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. // 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. // If both Work and Upper are empty strings, upperdir and workdir is omitted and the overlay is mounted readonly.
Upper string Upper *Absolute
// formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early; // formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early
Work string 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 ephemeral bool
} }
func (o *MountOverlayOp) early(*Params) error { func (o *MountOverlayOp) early(*Params) error {
if o.Work == zeroString { if o.Work == nil && o.Upper != nil {
switch o.Upper { switch o.Upper.String() {
case SourceTmpfsEphemeral: // ephemeral case FHSRoot: // ephemeral
o.ephemeral = true // intermediate root not yet available o.ephemeral = true // intermediate root not yet available
case zeroString: // readonly
default: default:
return msg.WrapErr(EINVAL, fmt.Sprintf("upperdir has unexpected value %q", o.Upper)) return msg.WrapErr(EINVAL, fmt.Sprintf("upperdir has unexpected value %q", o.Upper))
} }
} }
// readonly handled in apply
if !o.ephemeral { 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 // unreachable
return msg.WrapErr(ENOTRECOVERABLE, "impossible overlay state reached") return msg.WrapErr(ENOTRECOVERABLE, "impossible overlay state reached")
} }
if o.Upper != zeroString { if o.Upper != nil {
if !path.IsAbs(o.Upper) { if v, err := filepath.EvalSymlinks(o.Upper.String()); err != nil {
return msg.WrapErr(EBADE, fmt.Sprintf("upperdir %q is not absolute", o.Upper))
}
if v, err := filepath.EvalSymlinks(o.Upper); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} else { } else {
o.Upper = escapeOverlayDataSegment(toHost(v)) o.upper = escapeOverlayDataSegment(toHost(v))
} }
} }
if o.Work != zeroString { if o.Work != nil {
if !path.IsAbs(o.Work) { if v, err := filepath.EvalSymlinks(o.Work.String()); err != nil {
return msg.WrapErr(EBADE, fmt.Sprintf("workdir %q is not absolute", o.Work))
}
if v, err := filepath.EvalSymlinks(o.Work); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} else { } else {
o.Work = escapeOverlayDataSegment(toHost(v)) o.work = escapeOverlayDataSegment(toHost(v))
} }
} }
} }
for i := range o.Lower { o.lower = make([]string, len(o.Lower))
if !path.IsAbs(o.Lower[i]) { for i, a := range o.Lower {
return msg.WrapErr(EBADE, fmt.Sprintf("lowerdir %q is not absolute", o.Lower[i])) 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) return wrapErrSelf(err)
} else { } else {
o.Lower[i] = escapeOverlayDataSegment(toHost(v)) o.lower[i] = escapeOverlayDataSegment(toHost(v))
} }
} }
return nil return nil
} }
func (o *MountOverlayOp) apply(params *Params) error { func (o *MountOverlayOp) apply(params *Params) error {
if !path.IsAbs(o.Target) { if o.Target == nil {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", o.Target)) return EBADE
} }
target := toSysroot(o.Target) target := toSysroot(o.Target.String())
if err := os.MkdirAll(target, params.ParentPerm); err != nil { if err := os.MkdirAll(target, params.ParentPerm); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} }
@ -458,17 +456,17 @@ func (o *MountOverlayOp) apply(params *Params) error {
if o.ephemeral { if o.ephemeral {
var err error var err error
// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed // these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
if o.Upper, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil { if o.upper, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil {
return wrapErrSelf(err) 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) return wrapErrSelf(err)
} }
} }
options := make([]string, 0, 4) 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 { if len(o.Lower) < 2 {
return msg.WrapErr(EINVAL, "readonly overlay requires at least two lowerdir") 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") return msg.WrapErr(EINVAL, "overlay requires at least one lowerdir")
} }
options = append(options, options = append(options,
OptionOverlayUpperdir+"="+o.Upper, OptionOverlayUpperdir+"="+o.upper,
OptionOverlayWorkdir+"="+o.Work) OptionOverlayWorkdir+"="+o.work)
} }
options = append(options, options = append(options,
OptionOverlayLowerdir+"="+strings.Join(o.Lower, SpecialOverlayPath), OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath),
OptionOverlayUserxattr) OptionOverlayUserxattr)
return wrapErrSuffix(Mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)), 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)) } func init() { gob.Register(new(SymlinkOp)) }
// Link appends an [Op] that creates a symlink in the container filesystem. // Link appends an [Op] that creates a symlink in the container filesystem.
func (f *Ops) Link(target, linkName string) *Ops { func (f *Ops) Link(target *Absolute, linkName string, dereference bool) *Ops {
*f = append(*f, &SymlinkOp{target, linkName}) *f = append(*f, &SymlinkOp{target, linkName, dereference})
return f 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 { func (l *SymlinkOp) early(*Params) error {
if strings.HasPrefix(l[0], "*") { if l.Dereference {
l[0] = l[0][1:] if !isAbs(l.LinkName) {
if !path.IsAbs(l[0]) { return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l.LinkName))
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[0]))
} }
if name, err := os.Readlink(l[0]); err != nil { if name, err := os.Readlink(l.LinkName); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} else { } else {
l[0] = name l.LinkName = name
} }
} }
return nil 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 { if err := os.MkdirAll(path.Dir(target), params.ParentPerm); err != nil {
return wrapErrSelf(err) 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 wrapErrSelf(err)
} }
return nil return nil
} }
func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl } func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl }
func (*SymlinkOp) prefix() string { return "creating" } 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) String() string {
return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName)
}
func init() { gob.Register(new(MkdirOp)) } func init() { gob.Register(new(MkdirOp)) }
// Mkdir appends an [Op] that creates a directory in the container filesystem. // Mkdir appends an [Op] that creates a directory in the container filesystem.
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops { func (f *Ops) Mkdir(name *Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{dest, perm}) *f = append(*f, &MkdirOp{name, perm})
return f return f
} }
type MkdirOp struct { type MkdirOp struct {
Path string Path *Absolute
Perm os.FileMode Perm os.FileMode
} }
func (m *MkdirOp) early(*Params) error { return nil } func (m *MkdirOp) early(*Params) error { return nil }
func (m *MkdirOp) apply(*Params) error { func (m *MkdirOp) apply(*Params) error {
if !path.IsAbs(m.Path) { if m.Path == nil {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", m.Path)) return EBADE
} }
return wrapErrSelf(os.MkdirAll(toSysroot(m.Path.String()), m.Perm))
if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil {
return wrapErrSelf(err)
}
return nil
} }
func (m *MkdirOp) Is(op Op) bool { vm, ok := op.(*MkdirOp); return ok && m == vm } 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)) } func init() { gob.Register(new(TmpfileOp)) }
// Place appends an [Op] that places a file in container path [TmpfileOp.Path] containing [TmpfileOp.Data]. // 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. // 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} t := &TmpfileOp{Path: name}
*dataP = &t.Data *dataP = &t.Data
@ -590,14 +594,14 @@ func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
} }
type TmpfileOp struct { type TmpfileOp struct {
Path string Path *Absolute
Data []byte Data []byte
} }
func (t *TmpfileOp) early(*Params) error { return nil } func (t *TmpfileOp) early(*Params) error { return nil }
func (t *TmpfileOp) apply(params *Params) error { func (t *TmpfileOp) apply(params *Params) error {
if !path.IsAbs(t.Path) { if t.Path == nil {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) return EBADE
} }
var tmpPath string var tmpPath string
@ -613,7 +617,7 @@ func (t *TmpfileOp) apply(params *Params) error {
tmpPath = f.Name() tmpPath = f.Name()
} }
target := toSysroot(t.Path) target := toSysroot(t.Path.String())
if err := ensureFile(target, 0444, params.ParentPerm); err != nil { if err := ensureFile(target, 0444, params.ParentPerm); err != nil {
return err return err
} else if err = hostProc.bindMount( } else if err = hostProc.bindMount(

View File

@ -13,6 +13,8 @@ import (
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
) )
/* constants in this file bypass abs check, be extremely careful when changing them! */
const ( const (
// FHSRoot points to the file system root. // FHSRoot points to the file system root.
FHSRoot = "/" FHSRoot = "/"
@ -49,6 +51,35 @@ const (
FHSSys = "/sys/" 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 us [FHSRunUser] as [Absolute].
AbsFHSRunUser = &Absolute{FHSRunUser}
// 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 ( const (
// Nonexistent is a path that cannot exist. // Nonexistent is a path that cannot exist.
// /proc is chosen because a system with covered /proc is unsupported by this package. // /proc is chosen because a system with covered /proc is unsupported by this package.

View File

@ -26,7 +26,7 @@ func New(
var args []string var args []string
h := new(helperContainer) h := new(helperContainer)
h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles) 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 h.WaitDelay = WaitDelay
if cmdF != nil { if cmdF != nil {
cmdF(h.Container) cmdF(h.Container)

View File

@ -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 { 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) { return helper.New(ctx, container.MustAbs(os.Args[0]), "helper", argsWt, stat, argF, func(z *container.Container) {
setOutput(&z.Stdout, &z.Stderr) 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) }, nil)
}) })
}) })

View File

@ -2,12 +2,15 @@
package hst package hst
import ( import (
"hakurei.app/container"
"hakurei.app/system" "hakurei.app/system"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
const Tmp = "/.hakurei" const Tmp = "/.hakurei"
var AbsTmp = container.MustAbs(Tmp)
// Config is used to seal an app implementation. // Config is used to seal an app implementation.
type Config struct { type Config struct {
// reverse-DNS style arbitrary identifier string from config; // reverse-DNS style arbitrary identifier string from config;
@ -16,7 +19,7 @@ type Config struct {
ID string `json:"id"` ID string `json:"id"`
// absolute path to executable file // absolute path to executable file
Path string `json:"path,omitempty"` Path *container.Absolute `json:"path,omitempty"`
// final args passed to container init // final args passed to container init
Args []string `json:"args"` Args []string `json:"args"`
@ -35,12 +38,12 @@ type Config struct {
// passwd username in container, defaults to passwd name of target uid or chronos // passwd username in container, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
// absolute path to shell, empty for host shell // absolute path to shell
Shell string `json:"shell,omitempty"` Shell *container.Absolute `json:"shell"`
// absolute path to home directory in the init mount namespace // absolute path to home directory in the init mount namespace
Data string `json:"data"` Data *container.Absolute `json:"data"`
// directory to enter and use as home in the container mount namespace, empty for Data // directory to enter and use as home in the container mount namespace, nil for Data
Dir string `json:"dir"` Dir *container.Absolute `json:"dir,omitempty"`
// extra acl ops, dispatches before container init // extra acl ops, dispatches before container init
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"` ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
@ -55,21 +58,24 @@ type Config struct {
// ExtraPermConfig describes an acl update op. // ExtraPermConfig describes an acl update op.
type ExtraPermConfig struct { type ExtraPermConfig struct {
Ensure bool `json:"ensure,omitempty"` Ensure bool `json:"ensure,omitempty"`
Path string `json:"path"` Path *container.Absolute `json:"path"`
Read bool `json:"r,omitempty"` Read bool `json:"r,omitempty"`
Write bool `json:"w,omitempty"` Write bool `json:"w,omitempty"`
Execute bool `json:"x,omitempty"` Execute bool `json:"x,omitempty"`
} }
func (e *ExtraPermConfig) String() string { func (e *ExtraPermConfig) String() string {
buf := make([]byte, 0, 5+len(e.Path)) if e.Path == nil {
return "<invalid>"
}
buf := make([]byte, 0, 5+len(e.Path.String()))
buf = append(buf, '-', '-', '-') buf = append(buf, '-', '-', '-')
if e.Ensure { if e.Ensure {
buf = append(buf, '+') buf = append(buf, '+')
} }
buf = append(buf, ':') buf = append(buf, ':')
buf = append(buf, []byte(e.Path)...) buf = append(buf, []byte(e.Path.String())...)
if e.Read { if e.Read {
buf[0] = 'r' buf[0] = 'r'
} }

View File

@ -3,14 +3,11 @@ package hst
import ( import (
"time" "time"
"hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
) )
const ( 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 // TmpfsPerm is the permission bits for tmpfs mount points
// configured through [FilesystemConfig]. // configured through [FilesystemConfig].
TmpfsPerm = 0755 TmpfsPerm = 0755
@ -55,18 +52,18 @@ type (
// pass through all devices // pass through all devices
Device bool `json:"device,omitempty"` Device bool `json:"device,omitempty"`
// container host filesystem bind mounts // container host filesystem bind mounts
Filesystem []*FilesystemConfig `json:"filesystem"` Filesystem []FilesystemConfig `json:"filesystem"`
// create symlinks inside container filesystem // create symlinks inside container filesystem
Link [][2]string `json:"symlink"` Link []LinkConfig `json:"symlink"`
// automatically bind mount top-level directories to container root; // automatically bind mount top-level directories to container root;
// the zero value disables this behaviour // the zero value disables this behaviour
AutoRoot string `json:"auto_root,omitempty"` AutoRoot *container.Absolute `json:"auto_root,omitempty"`
// extra flags for AutoRoot // extra flags for AutoRoot
RootFlags int `json:"root_flags,omitempty"` RootFlags int `json:"root_flags,omitempty"`
// read-only /etc directory // read-only /etc directory
Etc string `json:"etc,omitempty"` Etc *container.Absolute `json:"etc,omitempty"`
// automatically set up /etc symlinks // automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"` AutoEtc bool `json:"auto_etc"`
} }
@ -74,9 +71,9 @@ type (
// FilesystemConfig is an abstract representation of a bind mount. // FilesystemConfig is an abstract representation of a bind mount.
FilesystemConfig struct { FilesystemConfig struct {
// mount point in container, same as src if empty // 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 // host filesystem path to make available to the container
Src string `json:"src"` Src *container.Absolute `json:"src"`
// do not mount filesystem read-only // do not mount filesystem read-only
Write bool `json:"write,omitempty"` Write bool `json:"write,omitempty"`
// do not disable device files // do not disable device files
@ -84,4 +81,12 @@ type (
// fail if the bind mount cannot be established for any reason // fail if the bind mount cannot be established for any reason
Must bool `json:"require,omitempty"` 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"`
}
) )

View File

@ -1,11 +1,15 @@
package hst package hst
import "hakurei.app/container"
// Paths contains environment-dependent paths used by hakurei. // Paths contains environment-dependent paths used by hakurei.
type Paths struct { type Paths struct {
// temporary directory returned by [os.TempDir] (usually `/tmp`)
TempDir *container.Absolute `json:"temp_dir"`
// path to shared directory (usually `/tmp/hakurei.%d`) // 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`) // 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`) // application runtime directory (usually `/run/user/%d/hakurei`)
RunDirPath string `json:"run_dir_path"` RunDirPath *container.Absolute `json:"run_dir_path"`
} }

View File

@ -12,7 +12,7 @@ func Template() *Config {
return &Config{ return &Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Path: container.FHSRun + "current-system/sw/bin/chromium", Path: container.AbsFHSRun.Append("current-system/sw/bin/chromium"),
Args: []string{ Args: []string{
"chromium", "chromium",
"--ignore-gpu-blocklist", "--ignore-gpu-blocklist",
@ -46,12 +46,12 @@ func Template() *Config {
DirectWayland: false, DirectWayland: false,
Username: "chronos", Username: "chronos",
Shell: container.FHSRun + "current-system/sw/bin/zsh", Shell: container.AbsFHSRun.Append("current-system/sw/bin/zsh"),
Data: container.FHSVarLib + "hakurei/u0/org.chromium.Chromium", Data: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"),
Dir: "/data/data/org.chromium.Chromium", Dir: container.MustAbs("/data/data/org.chromium.Chromium"),
ExtraPerms: []*ExtraPermConfig{ ExtraPerms: []*ExtraPermConfig{
{Path: container.FHSVarLib + "hakurei/u0", Ensure: true, Execute: true}, {Path: container.AbsFHSVarLib.Append("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/org.chromium.Chromium"), Read: true, Write: true, Execute: true},
}, },
Identity: 9, Identity: 9,
@ -77,20 +77,20 @@ func Template() *Config {
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT", "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
}, },
Filesystem: []*FilesystemConfig{ Filesystem: []FilesystemConfig{
{Dst: container.FHSTmp, Src: SourceTmpfs, Write: true}, {Dst: container.AbsFHSTmp, Src: container.AbsNonexistent, Write: true},
{Src: "/nix/store"}, {Src: container.MustAbs("/nix/store")},
{Src: container.FHSRun + "current-system"}, {Src: container.AbsFHSRun.Append("current-system")},
{Src: container.FHSRun + "opengl-driver"}, {Src: container.AbsFHSRun.Append("opengl-driver")},
{Src: container.FHSVar + "db/nix-channels"}, {Src: container.AbsFHSVar.Append("db/nix-channels")},
{Src: container.FHSVarLib + "hakurei/u0/org.chromium.Chromium", {Src: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"),
Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true}, Dst: container.MustAbs("/data/data/org.chromium.Chromium"), Write: true, Must: true},
{Src: container.FHSDev + "dri", Device: true}, {Src: container.AbsFHSDev.Append("dri"), Device: true},
}, },
Link: [][2]string{{container.FHSRunUser + "65534", container.FHSRunUser + "150"}}, Link: []LinkConfig{{container.AbsFHSRunUser.Append("65534"), container.FHSRunUser + "150"}},
AutoRoot: container.FHSVarLib + "hakurei/base/org.debian", AutoRoot: container.AbsFHSVarLib.Append("hakurei/base/org.debian"),
RootFlags: container.BindWritable, RootFlags: container.BindWritable,
Etc: container.FHSEtc, Etc: container.AbsFHSEtc,
AutoEtc: true, AutoEtc: true,
}, },
} }

View File

@ -99,7 +99,7 @@ func TestTemplate(t *testing.T) {
"filesystem": [ "filesystem": [
{ {
"dst": "/tmp/", "dst": "/tmp/",
"src": "tmpfs", "src": "/proc/nonexistent",
"write": true "write": true
}, },
{ {
@ -126,10 +126,10 @@ func TestTemplate(t *testing.T) {
} }
], ],
"symlink": [ "symlink": [
[ {
"/run/user/65534", "target": "/run/user/65534",
"/run/user/150" "linkname": "/run/user/150"
] }
], ],
"auto_root": "/var/lib/hakurei/base/org.debian", "auto_root": "/var/lib/hakurei/base/org.debian",
"root_flags": 2, "root_flags": 2,

View File

@ -7,7 +7,6 @@ import (
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys" "hakurei.app/internal/sys"
) )
@ -59,10 +58,6 @@ func (a *app) Seal(config *hst.Config) (SealedApp, error) {
if a.outcome != nil { if a.outcome != nil {
panic("app sealed twice") panic("app sealed twice")
} }
if config == nil {
return nil, hlog.WrapErr(ErrConfig,
"attempted to seal app with nil config")
}
seal := new(outcome) seal := new(outcome)
seal.id = a.id seal.id = a.id

View File

@ -11,6 +11,7 @@ import (
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app" "hakurei.app/internal/app"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys" "hakurei.app/internal/sys"
"hakurei.app/system" "hakurei.app/system"
) )
@ -36,6 +37,7 @@ func TestApp(t *testing.T) {
) )
if !t.Run("seal", func(t *testing.T) { if !t.Run("seal", func(t *testing.T) {
if sa, err := a.Seal(tc.config); err != nil { if sa, err := a.Seal(tc.config); err != nil {
hlog.PrintBaseError(err, "got generic error:")
t.Errorf("Seal: error = %v", err) t.Errorf("Seal: error = %v", err)
return return
} else { } else {

View File

@ -12,21 +12,24 @@ import (
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
func m(pathname string) *container.Absolute { return container.MustAbs(pathname) }
var testCasesNixos = []sealTestCase{ var testCasesNixos = []sealTestCase{
{ {
"nixos chromium direct wayland", new(stubNixOS), "nixos chromium direct wayland", new(stubNixOS),
&hst.Config{ &hst.Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start", Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
Enablements: system.EWayland | system.EDBus | system.EPulse, Enablements: system.EWayland | system.EDBus | system.EPulse,
Shell: m("/run/current-system/sw/bin/zsh"),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Userns: true, Net: true, MapRealUID: true, Env: nil, AutoEtc: true, Userns: true, Net: true, MapRealUID: true, Env: nil, AutoEtc: true,
Filesystem: []*hst.FilesystemConfig{ Filesystem: []hst.FilesystemConfig{
{Src: "/bin", Must: true}, {Src: "/usr/bin/", Must: true}, {Src: m("/bin"), Must: true}, {Src: m("/usr/bin/"), Must: true},
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true}, {Src: m("/nix/store"), Must: true}, {Src: m("/run/current-system"), Must: true},
{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"}, {Src: m("/sys/block")}, {Src: m("/sys/bus")}, {Src: m("/sys/class")}, {Src: m("/sys/dev")}, {Src: m("/sys/devices")},
{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true}, {Src: m("/run/opengl-driver"), Must: true}, {Src: m("/dev/dri"), Device: true},
}, },
}, },
SystemBus: &dbus.Config{ SystemBus: &dbus.Config{
@ -50,7 +53,7 @@ var testCasesNixos = []sealTestCase{
DirectWayland: true, DirectWayland: true,
Username: "u0_a1", Username: "u0_a1",
Data: "/var/lib/persist/module/hakurei/0/1", Data: m("/var/lib/persist/module/hakurei/0/1"),
Identity: 1, Groups: []string{}, Identity: 1, Groups: []string{},
}, },
state.ID{ state.ID{
@ -98,8 +101,8 @@ var testCasesNixos = []sealTestCase{
&container.Params{ &container.Params{
Uid: 1971, Uid: 1971,
Gid: 100, Gid: 100,
Dir: "/var/lib/persist/module/hakurei/0/1", Dir: m("/var/lib/persist/module/hakurei/0/1"),
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start", Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"}, Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
Env: []string{ Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
@ -116,34 +119,34 @@ var testCasesNixos = []sealTestCase{
"XDG_SESSION_TYPE=tty", "XDG_SESSION_TYPE=tty",
}, },
Ops: new(container.Ops). Ops: new(container.Ops).
Proc("/proc/"). Proc(m("/proc/")).
Tmpfs(hst.Tmp, 4096, 0755). Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable("/dev/", true). DevWritable(m("/dev/"), true).
Bind("/bin", "/bin", 0). Bind(m("/bin"), m("/bin"), 0).
Bind("/usr/bin/", "/usr/bin/", 0). Bind(m("/usr/bin/"), m("/usr/bin/"), 0).
Bind("/nix/store", "/nix/store", 0). Bind(m("/nix/store"), m("/nix/store"), 0).
Bind("/run/current-system", "/run/current-system", 0). Bind(m("/run/current-system"), m("/run/current-system"), 0).
Bind("/sys/block", "/sys/block", container.BindOptional). Bind(m("/sys/block"), m("/sys/block"), container.BindOptional).
Bind("/sys/bus", "/sys/bus", container.BindOptional). Bind(m("/sys/bus"), m("/sys/bus"), container.BindOptional).
Bind("/sys/class", "/sys/class", container.BindOptional). Bind(m("/sys/class"), m("/sys/class"), container.BindOptional).
Bind("/sys/dev", "/sys/dev", container.BindOptional). Bind(m("/sys/dev"), m("/sys/dev"), container.BindOptional).
Bind("/sys/devices", "/sys/devices", container.BindOptional). Bind(m("/sys/devices"), m("/sys/devices"), container.BindOptional).
Bind("/run/opengl-driver", "/run/opengl-driver", 0). Bind(m("/run/opengl-driver"), m("/run/opengl-driver"), 0).
Bind("/dev/dri", "/dev/dri", container.BindDevice|container.BindWritable|container.BindOptional). Bind(m("/dev/dri"), m("/dev/dri"), container.BindDevice|container.BindWritable|container.BindOptional).
Etc("/etc/", "8e2c76b066dabe574cf073bdb46eb5c1"). Etc(m("/etc/"), "8e2c76b066dabe574cf073bdb46eb5c1").
Remount("/dev/", syscall.MS_RDONLY). Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs("/run/user/", 4096, 0755). Tmpfs(m("/run/user/"), 4096, 0755).
Bind("/tmp/hakurei.1971/runtime/1", "/run/user/1971", container.BindWritable). Bind(m("/tmp/hakurei.1971/runtime/1"), m("/run/user/1971"), container.BindWritable).
Bind("/tmp/hakurei.1971/tmpdir/1", "/tmp/", container.BindWritable). Bind(m("/tmp/hakurei.1971/tmpdir/1"), m("/tmp/"), container.BindWritable).
Bind("/var/lib/persist/module/hakurei/0/1", "/var/lib/persist/module/hakurei/0/1", container.BindWritable). Bind(m("/var/lib/persist/module/hakurei/0/1"), m("/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(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("/etc/group", []byte("hakurei:x:100:\n")). Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0). Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0).
Bind("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0). Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0).
Place(hst.Tmp+"/pulse-cookie", nil). Place(m(hst.Tmp+"/pulse-cookie"), nil).
Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0). Bind(m("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0). Bind(m("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
Remount("/", syscall.MS_RDONLY), Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
ForwardCancel: true, ForwardCancel: true,

View File

@ -16,7 +16,7 @@ import (
var testCasesPd = []sealTestCase{ var testCasesPd = []sealTestCase{
{ {
"nixos permissive defaults no enablements", new(stubNixOS), "nixos permissive defaults no enablements", new(stubNixOS),
&hst.Config{Username: "chronos", Data: "/home/chronos"}, &hst.Config{Username: "chronos", Data: m("/home/chronos")},
state.ID{ state.ID{
0x4a, 0x45, 0x0b, 0x65, 0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15, 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", 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), Ensure("/tmp/hakurei.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
&container.Params{ &container.Params{
Dir: "/home/chronos", Dir: m("/home/chronos"),
Path: "/run/current-system/sw/bin/zsh", Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"/run/current-system/sw/bin/zsh"}, Args: []string{"/run/current-system/sw/bin/zsh"},
Env: []string{ Env: []string{
"HOME=/home/chronos", "HOME=/home/chronos",
@ -43,23 +43,23 @@ var testCasesPd = []sealTestCase{
"XDG_SESSION_TYPE=tty", "XDG_SESSION_TYPE=tty",
}, },
Ops: new(container.Ops). Ops: new(container.Ops).
Root("/", "4a450b6596d7bc15bd01780eb9a607ac", container.BindWritable). Root(m("/"), "4a450b6596d7bc15bd01780eb9a607ac", container.BindWritable).
Proc("/proc/"). Proc(m("/proc/")).
Tmpfs(hst.Tmp, 4096, 0755). Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable("/dev/", true). DevWritable(m("/dev/"), true).
Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional). Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
Readonly("/var/run/nscd", 0755). Readonly(m("/var/run/nscd"), 0755).
Tmpfs("/run/user/1971", 8192, 0755). Tmpfs(m("/run/user/1971"), 8192, 0755).
Tmpfs("/run/dbus", 8192, 0755). Tmpfs(m("/run/dbus"), 8192, 0755).
Etc("/etc/", "4a450b6596d7bc15bd01780eb9a607ac"). Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac").
Remount("/dev/", syscall.MS_RDONLY). Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs("/run/user/", 4096, 0755). Tmpfs(m("/run/user/"), 4096, 0755).
Bind("/tmp/hakurei.1971/runtime/0", "/run/user/65534", container.BindWritable). Bind(m("/tmp/hakurei.1971/runtime/0"), m("/run/user/65534"), container.BindWritable).
Bind("/tmp/hakurei.1971/tmpdir/0", "/tmp/", container.BindWritable). Bind(m("/tmp/hakurei.1971/tmpdir/0"), m("/tmp/"), container.BindWritable).
Bind("/home/chronos", "/home/chronos", container.BindWritable). Bind(m("/home/chronos"), m("/home/chronos"), container.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). Place(m("/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")). Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
Remount("/", syscall.MS_RDONLY), Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
RetainSession: true, RetainSession: true,
@ -74,7 +74,7 @@ var testCasesPd = []sealTestCase{
Identity: 9, Identity: 9,
Groups: []string{"video"}, Groups: []string{"video"},
Username: "chronos", Username: "chronos",
Data: "/home/chronos", Data: m("/home/chronos"),
SessionBus: &dbus.Config{ SessionBus: &dbus.Config{
Talk: []string{ Talk: []string{
"org.freedesktop.Notifications", "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/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write), UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
&container.Params{ &container.Params{
Dir: "/home/chronos", Dir: m("/home/chronos"),
Path: "/run/current-system/sw/bin/zsh", Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"zsh", "-c", "exec chromium "}, Args: []string{"zsh", "-c", "exec chromium "},
Env: []string{ Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
@ -178,29 +178,29 @@ var testCasesPd = []sealTestCase{
"XDG_SESSION_TYPE=tty", "XDG_SESSION_TYPE=tty",
}, },
Ops: new(container.Ops). Ops: new(container.Ops).
Root("/", "ebf083d1b175911782d413369b64ce7c", container.BindWritable). Root(m("/"), "ebf083d1b175911782d413369b64ce7c", container.BindWritable).
Proc("/proc/"). Proc(m("/proc/")).
Tmpfs(hst.Tmp, 4096, 0755). Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable("/dev/", true). DevWritable(m("/dev/"), true).
Bind("/dev/dri", "/dev/dri", container.BindWritable|container.BindDevice|container.BindOptional). Bind(m("/dev/dri"), m("/dev/dri"), container.BindWritable|container.BindDevice|container.BindOptional).
Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional). Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
Readonly("/var/run/nscd", 0755). Readonly(m("/var/run/nscd"), 0755).
Tmpfs("/run/user/1971", 8192, 0755). Tmpfs(m("/run/user/1971"), 8192, 0755).
Tmpfs("/run/dbus", 8192, 0755). Tmpfs(m("/run/dbus"), 8192, 0755).
Etc("/etc/", "ebf083d1b175911782d413369b64ce7c"). Etc(m("/etc/"), "ebf083d1b175911782d413369b64ce7c").
Remount("/dev/", syscall.MS_RDONLY). Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs("/run/user/", 4096, 0755). Tmpfs(m("/run/user/"), 4096, 0755).
Bind("/tmp/hakurei.1971/runtime/9", "/run/user/65534", container.BindWritable). Bind(m("/tmp/hakurei.1971/runtime/9"), m("/run/user/65534"), container.BindWritable).
Bind("/tmp/hakurei.1971/tmpdir/9", "/tmp/", container.BindWritable). Bind(m("/tmp/hakurei.1971/tmpdir/9"), m("/tmp/"), container.BindWritable).
Bind("/home/chronos", "/home/chronos", container.BindWritable). Bind(m("/home/chronos"), m("/home/chronos"), container.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). Place(m("/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")). Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0", 0). Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0).
Bind("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0). Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0).
Place(hst.Tmp+"/pulse-cookie", nil). Place(m(hst.Tmp+"/pulse-cookie"), nil).
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0). Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0).
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0). Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
Remount("/", syscall.MS_RDONLY), Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
RetainSession: true, RetainSession: true,

View File

@ -127,8 +127,8 @@ func (s *stubNixOS) Open(name string) (fs.File, error) {
func (s *stubNixOS) Paths() hst.Paths { func (s *stubNixOS) Paths() hst.Paths {
return hst.Paths{ return hst.Paths{
SharePath: "/tmp/hakurei.1971", SharePath: m("/tmp/hakurei.1971"),
RuntimePath: "/run/user/1971", RuntimePath: m("/run/user/1971"),
RunDirPath: "/run/user/1971/hakurei", RunDirPath: m("/run/user/1971/hakurei"),
} }
} }

View File

@ -12,6 +12,7 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys" "hakurei.app/internal/sys"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -24,7 +25,7 @@ const preallocateOpsCount = 1 << 5
// Note that remaining container setup must be queued by the caller. // Note that remaining container setup must be queued by the caller.
func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid *int) (*container.Params, map[string]string, error) { func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid *int) (*container.Params, map[string]string, error) {
if s == nil { if s == nil {
return nil, nil, syscall.EBADE return nil, nil, hlog.WrapErr(syscall.EBADE, "invalid container configuration")
} }
params := &container.Params{ params := &container.Params{
@ -73,21 +74,18 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
*gid = container.OverflowGid() *gid = container.OverflowGid()
} }
if s.AutoRoot != "" { if s.AutoRoot != nil {
if !path.IsAbs(s.AutoRoot) {
return nil, nil, fmt.Errorf("auto root target %q not absolute", s.AutoRoot)
}
params.Root(s.AutoRoot, prefix, s.RootFlags) params.Root(s.AutoRoot, prefix, s.RootFlags)
} }
params. params.
Proc(container.FHSProc). Proc(container.AbsFHSProc).
Tmpfs(hst.Tmp, 1<<12, 0755) Tmpfs(hst.AbsTmp, 1<<12, 0755)
if !s.Device { if !s.Device {
params.DevWritable(container.FHSDev, true) params.DevWritable(container.AbsFHSDev, true)
} else { } 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; /* 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 */ and should not be treated as such, ALWAYS be careful with what you bind */
var hidePaths []string var hidePaths []string
sc := os.Paths() sc := os.Paths()
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath) hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String())
_, systemBusAddr := dbus.Address() _, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil { if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
return nil, nil, err 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)) hidePathSource := make([][2]string, 0, len(s.Filesystem))
// AutoRoot is a collection of many BindMountOp internally // AutoRoot is a collection of many BindMountOp internally
if s.AutoRoot != "" { if s.AutoRoot != nil {
if d, err := os.ReadDir(s.AutoRoot); err != nil { if d, err := os.ReadDir(s.AutoRoot.String()); err != nil {
return nil, nil, err return nil, nil, err
} else { } else {
hidePathSource = slices.Grow(hidePathSource, len(d)) hidePathSource = slices.Grow(hidePathSource, len(d))
for _, ent := range d { for _, ent := range d {
name := ent.Name() name := ent.Name()
if container.IsAutoRootBindable(name) { if container.IsAutoRootBindable(name) {
name = path.Join(s.AutoRoot, name) name = path.Join(s.AutoRoot.String(), name)
srcP := [2]string{name, name} srcP := [2]string{name, name}
if err = evalSymlinks(os, &srcP[0]); err != nil { if err = evalSymlinks(os, &srcP[0]); err != nil {
return nil, nil, err 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 { for i, c := range s.Filesystem {
if c == nil { if c.Src == nil {
continue return nil, nil, fmt.Errorf("invalid filesystem at index %d", i)
} }
// special filesystems // special filesystems
switch c.Src { switch c.Src.String() {
case hst.SourceTmpfs: case container.Nonexistent:
if !path.IsAbs(c.Dst) { if c.Dst == nil {
return nil, nil, fmt.Errorf("tmpfs dst %q is not absolute", c.Dst) return nil, nil, errors.New("tmpfs dst must not be nil")
} }
if c.Write { if c.Write {
params.Tmpfs(c.Dst, hst.TmpfsSize, hst.TmpfsPerm) 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 continue
} }
if !path.IsAbs(c.Src) { dst := c.Dst
return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src) if dst == nil {
dst = c.Src
} }
dest := c.Dst p := [2]string{c.Src.String(), c.Src.String()}
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}
if err := evalSymlinks(os, &p[0]); err != nil { if err := evalSymlinks(os, &p[0]); err != nil {
return nil, nil, err return nil, nil, err
} }
@ -197,7 +189,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
if !c.Must { if !c.Must {
flags |= container.BindOptional flags |= container.BindOptional
} }
params.Bind(c.Src, dest, flags) params.Bind(c.Src, dst, flags)
} }
for _, p := range hidePathSource { for _, p := range hidePathSource {
@ -219,29 +211,49 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
// cover matched paths // cover matched paths
for i, ok := range hidePathMatch { for i, ok := range hidePathMatch {
if ok { 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 { for i, l := range s.Link {
params.Link(l[0], l[1]) 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.AutoEtc {
if s.Etc != "" { if s.Etc != nil {
params.Bind(s.Etc, container.FHSEtc, 0) params.Bind(s.Etc, container.AbsFHSEtc, 0)
} }
} else { } else {
etcPath := s.Etc if s.Etc == nil {
if etcPath == "" { params.Etc(container.AbsFHSEtc, prefix)
etcPath = container.FHSEtc } else {
params.Etc(s.Etc, prefix)
} }
params.Etc(etcPath, prefix)
} }
// no more ContainerConfig paths beyond this point // no more ContainerConfig paths beyond this point
if !s.Device { if !s.Device {
params.Remount(container.FHSDev, syscall.MS_RDONLY) params.Remount(container.AbsFHSDev, syscall.MS_RDONLY)
} }
return params, maps.Clone(s.Env), nil return params, maps.Clone(s.Env), nil

View File

@ -39,7 +39,7 @@ func (seal *outcome) Run(rs *RunState) error {
if err := seal.sys.Commit(seal.ctx); err != nil { if err := seal.sys.Commit(seal.ctx); err != nil {
return err 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 deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store
defer func() { defer func() {
var revertErr error var revertErr error
@ -128,7 +128,7 @@ func (seal *outcome) Run(rs *RunState) error {
os.Getpid(), os.Getpid(),
seal.waitDelay, seal.waitDelay,
seal.container, seal.container,
seal.user.data, seal.user.data.String(),
hlog.Load(), hlog.Load(),
}) })
}() }()

View File

@ -49,10 +49,8 @@ const (
) )
var ( var (
ErrConfig = errors.New("no configuration to seal") ErrIdent = errors.New("invalid identity")
ErrUser = errors.New("invalid aid") ErrName = errors.New("invalid username")
ErrHome = errors.New("invalid home directory")
ErrName = errors.New("invalid username")
ErrXDisplay = errors.New(display + " unset") 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 { type outcome struct {
// copied from initialising [app] // copied from initialising [app]
id *stringPair[state.ID] id *stringPair[state.ID]
// copied from [sys.State] response // copied from [sys.State]
runDirPath string runDirPath *container.Absolute
// initial [hst.Config] gob stream for state data; // initial [hst.Config] gob stream for state data;
// this is prepared ahead of time as config is clobbered during seal creation // 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 // whether XDG_RUNTIME_DIR is used post hsu
useRuntimeDir bool useRuntimeDir bool
// process-specific directory in tmpdir, empty if unused // process-specific directory in tmpdir, empty if unused
sharePath string sharePath *container.Absolute
// process-specific directory in XDG_RUNTIME_DIR, empty if unused // process-specific directory in XDG_RUNTIME_DIR, empty if unused
runtimeSharePath string runtimeSharePath *container.Absolute
seal *outcome seal *outcome
sc hst.Paths sc hst.Paths
@ -107,48 +105,48 @@ func (share *shareHost) ensureRuntimeDir() {
return return
} }
share.useRuntimeDir = true share.useRuntimeDir = true
share.seal.sys.Ensure(share.sc.RunDirPath, 0700) share.seal.sys.Ensure(share.sc.RunDirPath.String(), 0700)
share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath, acl.Execute) share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath.String(), acl.Execute)
share.seal.sys.Ensure(share.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset 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, acl.Execute) share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath.String(), acl.Execute)
} }
// instance returns a process-specific share path within tmpdir // instance returns a process-specific share path within tmpdir
func (share *shareHost) instance() string { func (share *shareHost) instance() *container.Absolute {
if share.sharePath != "" { if share.sharePath != nil {
return share.sharePath return share.sharePath
} }
share.sharePath = path.Join(share.sc.SharePath, share.seal.id.String()) share.sharePath = share.sc.SharePath.Append(share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.sharePath, 0711) share.seal.sys.Ephemeral(system.Process, share.sharePath.String(), 0711)
return share.sharePath return share.sharePath
} }
// runtime returns a process-specific share path within XDG_RUNTIME_DIR // runtime returns a process-specific share path within XDG_RUNTIME_DIR
func (share *shareHost) runtime() string { func (share *shareHost) runtime() *container.Absolute {
if share.runtimeSharePath != "" { if share.runtimeSharePath != nil {
return share.runtimeSharePath return share.runtimeSharePath
} }
share.ensureRuntimeDir() share.ensureRuntimeDir()
share.runtimeSharePath = path.Join(share.sc.RunDirPath, share.seal.id.String()) share.runtimeSharePath = share.sc.RunDirPath.Append(share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath, 0700) share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath.String(), 0700)
share.seal.sys.UpdatePerm(share.runtimeSharePath, acl.Execute) share.seal.sys.UpdatePerm(share.runtimeSharePath.String(), acl.Execute)
return share.runtimeSharePath return share.runtimeSharePath
} }
// hsuUser stores post-hsu credentials and metadata // hsuUser stores post-hsu credentials and metadata
type hsuUser struct { type hsuUser struct {
// application id // identity
aid *stringPair[int] aid *stringPair[int]
// target uid resolved by fid:aid // target uid resolved by hid:aid
uid *stringPair[int] uid *stringPair[int]
// supplementary group ids // supplementary group ids
supp []string supp []string
// home directory host path // home directory host path
data string data *container.Absolute
// app user home directory // app user home directory
home string home *container.Absolute
// passwd database username // passwd database username
username string username string
} }
@ -159,6 +157,13 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
seal.ctx = ctx 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 // encode initial configuration for state tracking
ct := new(bytes.Buffer) 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 // allowed aid range 0 to 9999, this is checked again in hsu
if config.Identity < 0 || config.Identity > 9999 { if config.Identity < 0 || config.Identity > 9999 {
return hlog.WrapErr(ErrUser, return hlog.WrapErr(ErrIdent,
fmt.Sprintf("identity %d out of range", config.Identity)) 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, return hlog.WrapErr(ErrName,
fmt.Sprintf("invalid user name %q", seal.user.username)) fmt.Sprintf("invalid user name %q", seal.user.username))
} }
if seal.user.data == "" || !path.IsAbs(seal.user.data) { if seal.user.home == nil {
return hlog.WrapErr(ErrHome,
fmt.Sprintf("invalid home directory %q", seal.user.data))
}
if seal.user.home == "" {
seal.user.home = seal.user.data seal.user.home = seal.user.data
} }
if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil { 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 // permissive defaults
if config.Container == nil { if config.Container == nil {
hlog.Verbose("container configuration not supplied, PROCEED WITH CAUTION") 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 // hsu clears the environment so resolve paths early
if !path.IsAbs(config.Path) { if config.Path == nil {
if len(config.Args) > 0 { if len(config.Args) > 0 {
if p, err := sys.LookPath(config.Args[0]); err != nil { if p, err := sys.LookPath(config.Args[0]); err != nil {
return hlog.WrapErr(err, err.Error()) return hlog.WrapErr(err, err.Error())
} else { } else if config.Path, err = container.NewAbs(p); err != nil {
config.Path = p return hlog.WrapErr(err, err.Error())
} }
} else { } else {
config.Path = config.Shell config.Path = config.Shell
@ -242,26 +242,34 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
Tty: true, Tty: true,
AutoEtc: true, AutoEtc: true,
AutoRoot: container.FHSRoot, AutoRoot: container.AbsFHSRoot,
RootFlags: container.BindWritable, RootFlags: container.BindWritable,
} }
// bind GPU stuff // bind GPU stuff
if config.Enablements&(system.EX11|system.EWayland) != 0 { 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 // 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 // hide nscd from container if present
const nscd = container.FHSVar + "run/nscd" nscd := container.AbsFHSVar.Append("run/nscd")
if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) { if _, err := sys.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) {
conf.Filesystem = append(conf.Filesystem, &hst.FilesystemConfig{Dst: nscd, Src: hst.SourceTmpfs}) conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfig{Dst: nscd, Src: container.AbsNonexistent})
} }
config.Container = conf 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 mapuid, mapgid *stringPair[int]
{ {
var uid, gid 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, return hlog.WrapErrSuffix(err,
"cannot initialise container configuration:") "cannot initialise container configuration:")
} }
if !path.IsAbs(config.Path) {
return hlog.WrapErr(syscall.EINVAL,
"invalid program path")
}
if len(config.Args) == 0 { if len(config.Args) == 0 {
config.Args = []string{config.Path} config.Args = []string{config.Path.String()}
} }
seal.container.Path = config.Path seal.container.Path = config.Path
seal.container.Args = config.Args 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 // inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
innerRuntimeDir := path.Join(container.FHSRunUser, mapuid.String()) innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String())
seal.env[xdgRuntimeDir] = innerRuntimeDir seal.env[xdgRuntimeDir] = innerRuntimeDir.String()
seal.env[xdgSessionClass] = "user" seal.env[xdgSessionClass] = "user"
seal.env[xdgSessionType] = "tty" seal.env[xdgSessionType] = "tty"
share := &shareHost{seal: seal, sc: sys.Paths()} share := &shareHost{seal: seal, sc: sys.Paths()}
seal.runDirPath = share.sc.RunDirPath seal.runDirPath = share.sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap()) 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") runtimeDir := share.sc.SharePath.Append("runtime")
seal.sys.Ensure(runtimeDir, 0700) seal.sys.Ensure(runtimeDir.String(), 0700)
seal.sys.UpdatePermType(system.User, runtimeDir, acl.Execute) seal.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute)
runtimeDirInst := path.Join(runtimeDir, seal.user.aid.String()) runtimeDirInst := runtimeDir.Append(seal.user.aid.String())
seal.sys.Ensure(runtimeDirInst, 0700) seal.sys.Ensure(runtimeDirInst.String(), 0700)
seal.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute)
seal.container.Tmpfs(container.FHSRunUser, 1<<12, 0755) seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable) seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
} }
{ {
tmpdir := path.Join(share.sc.SharePath, "tmpdir") tmpdir := share.sc.SharePath.Append("tmpdir")
seal.sys.Ensure(tmpdir, 0700) seal.sys.Ensure(tmpdir.String(), 0700)
seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute) seal.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute)
tmpdirInst := path.Join(tmpdir, seal.user.aid.String()) tmpdirInst := tmpdir.Append(seal.user.aid.String())
seal.sys.Ensure(tmpdirInst, 01700) seal.sys.Ensure(tmpdirInst.String(), 01700)
seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute)
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp // mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
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" username := "chronos"
if seal.user.username != "" { if seal.user.username != "" {
username = seal.user.username username = seal.user.username
} }
seal.container.Bind(seal.user.data, homeDir, container.BindWritable) seal.container.Bind(seal.user.data, seal.user.home, container.BindWritable)
seal.container.Dir = homeDir seal.container.Dir = seal.user.home
seal.env["HOME"] = homeDir seal.env["HOME"] = seal.user.home.String()
seal.env["USER"] = username seal.env["USER"] = username
seal.env[shell] = config.Shell seal.env[shell] = config.Shell.String()
seal.container.Place(container.FHSEtc+"passwd", seal.container.Place(container.AbsFHSEtc.Append("passwd"),
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+homeDir+":"+config.Shell+"\n")) []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+seal.user.home.String()+":"+config.Shell.String()+"\n"))
seal.container.Place(container.FHSEtc+"group", seal.container.Place(container.AbsFHSEtc.Append("group"),
[]byte("hakurei:x:"+mapgid.String()+":\n")) []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 { if config.Enablements&system.EWayland != 0 {
// outer wayland socket (usually `/run/user/%d/wayland-%d`) // outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath string var socketPath *container.Absolute
if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok { if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok {
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName) hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
socketPath = path.Join(share.sc.RuntimePath, wayland.FallbackName) socketPath = share.sc.RuntimePath.Append(wayland.FallbackName)
} else if !path.IsAbs(name) { } else if a, err := container.NewAbs(name); err != nil {
socketPath = path.Join(share.sc.RuntimePath, name) socketPath = share.sc.RuntimePath.Append(name)
} else { } else {
socketPath = name socketPath = a
} }
innerPath := path.Join(innerRuntimeDir, wayland.FallbackName) innerPath := innerRuntimeDir.Append(wayland.FallbackName)
seal.env[wayland.WaylandDisplay] = wayland.FallbackName seal.env[wayland.WaylandDisplay] = wayland.FallbackName
if !config.DirectWayland { // set up security-context-v1 if !config.DirectWayland { // set up security-context-v1
@ -370,14 +370,14 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
appID = "app.hakurei." + seal.id.String() appID = "app.hakurei." + seal.id.String()
} }
// downstream socket paths // downstream socket paths
outerPath := path.Join(share.instance(), "wayland") outerPath := share.instance().Append("wayland")
seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String()) seal.sys.Wayland(&seal.sync, outerPath.String(), socketPath.String(), appID, seal.id.String())
seal.container.Bind(outerPath, innerPath, 0) seal.container.Bind(outerPath, innerPath, 0)
} else { // bind mount wayland socket (insecure) } else { // bind mount wayland socket (insecure)
hlog.Verbose("direct wayland access, PROCEED WITH CAUTION") hlog.Verbose("direct wayland access, PROCEED WITH CAUTION")
share.ensureRuntimeDir() share.ensureRuntimeDir()
seal.container.Bind(socketPath, innerPath, 0) 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 { } else {
seal.sys.ChangeHosts("#" + seal.user.uid.String()) seal.sys.ChangeHosts("#" + seal.user.uid.String())
seal.env[display] = d 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 { if config.Enablements&system.EPulse != 0 {
// PulseAudio runtime directory (usually `/run/user/%d/pulse`) // 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`) // 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) { if !errors.Is(err, fs.ErrNotExist) {
return hlog.WrapErrSuffix(err, return hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir)) 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)) 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) { if !errors.Is(err, fs.ErrNotExist) {
return hlog.WrapErrSuffix(err, return hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket)) 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 // hard link pulse socket into target-executable share
innerPulseRuntimeDir := path.Join(share.runtime(), "pulse") innerPulseRuntimeDir := share.runtime().Append("pulse")
innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native") innerPulseSocket := innerRuntimeDir.Append("pulse", "native")
seal.sys.Link(pulseSocket, innerPulseRuntimeDir) seal.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String())
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) 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 // publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(sys); err != nil { if src, err := discoverPulseCookie(sys); err != nil {
// not fatal // not fatal
hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message())) hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message()))
} else { } else {
innerDst := hst.Tmp + "/pulse-cookie" innerDst := hst.AbsTmp.Append("/pulse-cookie")
seal.env[pulseCookie] = innerDst seal.env[pulseCookie] = innerDst.String()
var payload *[]byte var payload *[]byte
seal.container.PlaceP(innerDst, &payload) seal.container.PlaceP(innerDst, &payload)
seal.sys.CopyFile(payload, src, 256, 256) 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 // downstream socket paths
sharePath := share.instance() sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket")
sessionPath, systemPath := path.Join(sharePath, "bus"), path.Join(sharePath, "system_bus_socket")
// configure dbus proxy // configure dbus proxy
if f, err := seal.sys.ProxyDBus( if f, err := seal.sys.ProxyDBus(
config.SessionBus, config.SystemBus, config.SessionBus, config.SystemBus,
sessionPath, systemPath, sessionPath.String(), systemPath.String(),
); err != nil { ); err != nil {
return err return err
} else { } else {
@ -462,20 +462,20 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
// share proxy sockets // share proxy sockets
sessionInner := path.Join(innerRuntimeDir, "bus") sessionInner := innerRuntimeDir.Append("bus")
seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String()
seal.container.Bind(sessionPath, sessionInner, 0) 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 { if config.SystemBus != nil {
systemInner := container.FHSRun + "dbus/system_bus_socket" systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket")
seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String()
seal.container.Bind(systemPath, systemInner, 0) 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 // 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 // append ExtraPerms last
for _, p := range config.ExtraPerms { 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 { if p.Ensure {
seal.sys.Ensure(p.Path, 0700) seal.sys.Ensure(p.Path.String(), 0700)
} }
perms := make(acl.Perms, 0, 3) 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 { if p.Execute {
perms = append(perms, acl.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 // flatten and sort env for deterministic behaviour

View File

@ -3,10 +3,11 @@ package sys
import ( import (
"io/fs" "io/fs"
"log"
"os/user" "os/user"
"path"
"strconv" "strconv"
"hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
) )
@ -50,18 +51,23 @@ type State interface {
// CopyPaths is a generic implementation of [hst.Paths]. // CopyPaths is a generic implementation of [hst.Paths].
func CopyPaths(os State, v *hst.Paths) { func CopyPaths(os State, v *hst.Paths) {
v.SharePath = path.Join(os.TempDir(), "hakurei."+strconv.Itoa(os.Getuid())) if tempDir, err := container.NewAbs(os.TempDir()); err != nil {
log.Fatalf("invalid TMPDIR: %v", err)
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")
} else { } else {
v.RuntimePath = r v.TempDir = tempDir
v.RunDirPath = path.Join(v.RuntimePath, "hakurei")
} }
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) hlog.Verbosef("runtime directory at %q", v.RunDirPath)
} }

View File

@ -33,14 +33,17 @@ func Exec(ctx context.Context, p string) ([]*Entry, error) {
return nil, err return nil, err
} }
z := container.NewCommand(c, toolPath.String(), lddName, p) z := container.NewCommand(c, toolPath, lddName, p)
z.Hostname = "hakurei-" + lddName z.Hostname = "hakurei-" + lddName
z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompFlags |= seccomp.AllowMultiarch
z.SeccompPresets |= seccomp.PresetStrict z.SeccompPresets |= seccomp.PresetStrict
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer) stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
z.Stdout = stdout z.Stdout = stdout
z.Stderr = stderr 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 { if err := z.Start(); err != nil {
return nil, err return nil, err

View File

@ -1,21 +1,20 @@
package ldd package ldd
import ( import (
"path" "hakurei.app/container"
"slices"
) )
// Path returns a deterministic, deduplicated slice of absolute directory paths in entries. // Path returns a deterministic, deduplicated slice of absolute directory paths in entries.
func Path(entries []*Entry) []string { func Path(entries []*Entry) []*container.Absolute {
p := make([]string, 0, len(entries)*2) p := make([]*container.Absolute, 0, len(entries)*2)
for _, entry := range entries { for _, entry := range entries {
if path.IsAbs(entry.Path) { if a, err := container.NewAbs(entry.Path); err == nil {
p = append(p, path.Dir(entry.Path)) p = append(p, a.Dir())
} }
if path.IsAbs(entry.Name) { if a, err := container.NewAbs(entry.Name); err == nil {
p = append(p, path.Dir(entry.Name)) p = append(p, a.Dir())
} }
} }
slices.Sort(p) container.SortAbs(p)
return slices.Compact(p) return container.CompactAbs(p)
} }

View File

@ -124,6 +124,7 @@ in
username = getsubname fid app.identity; username = getsubname fid app.identity;
data = getsubhome fid app.identity; data = getsubhome fid app.identity;
inherit (cfg) shell;
inherit (app) identity groups; inherit (app) identity groups;
container = { container = {
@ -177,23 +178,23 @@ in
auto_etc = true; auto_etc = true;
symlink = [ symlink = [
[ {
"*/run/current-system" target = "/run/current-system";
"/run/current-system" linkname = "*/run/current-system";
] }
] ]
++ optionals (isGraphical && config.hardware.graphics.enable) ( ++ optionals (isGraphical && config.hardware.graphics.enable) (
[ [
[ {
config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument target = "/run/opengl-driver";
"/run/opengl-driver" linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument;
] }
] ]
++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [ ++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [
[ {
config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument target = "/run/opengl-driver-32";
/run/opengl-driver-32 linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument;
] }
] ]
); );
}; };

View File

@ -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 { stateDir = mkOption {
type = types.str; type = types.str;
description = '' description = ''

View File

@ -5,8 +5,6 @@ import (
"errors" "errors"
"os" "os"
"os/exec" "os/exec"
"path"
"slices"
"strconv" "strconv"
"syscall" "syscall"
@ -53,7 +51,7 @@ func (p *Proxy) Start() error {
toolPath = a toolPath = a
} }
var libPaths []string var libPaths []*container.Absolute
if entries, err := ldd.Exec(ctx, toolPath.String()); err != nil { if entries, err := ldd.Exec(ctx, toolPath.String()); err != nil {
return err return err
} else { } else {
@ -77,42 +75,46 @@ func (p *Proxy) Start() error {
} }
// upstream bus directories // 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 _, addr := range [][]AddrEntry{p.final.SessionUpstream, p.final.SystemUpstream} {
for _, ent := range addr { for _, ent := range addr {
if ent.Method != "unix" { if ent.Method != "unix" {
continue continue
} }
for _, pair := range ent.Values { for _, pair := range ent.Values {
if pair[0] != "path" || !path.IsAbs(pair[1]) { if pair[0] != "path" {
continue 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) container.SortAbs(upstreamPaths)
upstreamPaths = slices.Compact(upstreamPaths) upstreamPaths = container.CompactAbs(upstreamPaths)
for _, name := range upstreamPaths { for _, name := range upstreamPaths {
z.Bind(name, name, 0) z.Bind(name, name, 0)
} }
// parent directories of bind paths // parent directories of bind paths
sockDirPaths := make([]string, 0, 2) sockDirPaths := make([]*container.Absolute, 0, 2)
if d := path.Dir(p.final.Session[1]); path.IsAbs(d) { if a, err := container.NewAbs(p.final.Session[1]); err == nil {
sockDirPaths = append(sockDirPaths, d) sockDirPaths = append(sockDirPaths, a.Dir())
} }
if d := path.Dir(p.final.System[1]); path.IsAbs(d) { if a, err := container.NewAbs(p.final.System[1]); err == nil {
sockDirPaths = append(sockDirPaths, d) sockDirPaths = append(sockDirPaths, a.Dir())
} }
slices.Sort(sockDirPaths) container.SortAbs(sockDirPaths)
sockDirPaths = slices.Compact(sockDirPaths) sockDirPaths = container.CompactAbs(sockDirPaths)
for _, name := range sockDirPaths { for _, name := range sockDirPaths {
z.Bind(name, name, container.BindWritable) z.Bind(name, name, container.BindWritable)
} }
// xdg-dbus-proxy bin path // xdg-dbus-proxy bin path
binPath := path.Dir(toolPath.String()) binPath := toolPath.Dir()
z.Bind(binPath, binPath, 0) z.Bind(binPath, binPath, 0)
}, nil) }, nil)
} }