Compare commits

..

1 Commits

Author SHA1 Message Date
7add29b79c
container: optionally isolate host abstract UNIX domain sockets via landlock
Some checks failed
Test / Create distribution (push) Failing after 52s
Test / Sandbox (push) Successful in 2m22s
Test / Sandbox (race detector) (push) Successful in 4m4s
Test / Planterette (push) Successful in 4m3s
Test / Create distribution (pull_request) Failing after 29s
Test / Sandbox (pull_request) Successful in 40s
Test / Sandbox (race detector) (pull_request) Successful in 41s
Test / Planterette (pull_request) Successful in 41s
Test / Hakurei (push) Failing after 20m46s
Test / Hakurei (race detector) (push) Failing after 22m7s
Test / Flake checks (push) Has been skipped
Test / Hakurei (pull_request) Failing after 34m54s
Test / Hakurei (race detector) (pull_request) Failing after 36m37s
Test / Flake checks (pull_request) Has been skipped
2025-08-17 13:27:39 +09:00
107 changed files with 1820 additions and 5213 deletions

View File

@ -73,20 +73,20 @@ jobs:
path: result/* path: result/*
retention-days: 1 retention-days: 1
hpkg: planterette:
name: Hpkg name: Planterette
runs-on: nix runs-on: nix
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Run NixOS test - name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hpkg run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.planterette
- name: Upload test output - name: Upload test output
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: "hpkg-vm-output" name: "planterette-vm-output"
path: result/* path: result/*
retention-days: 1 retention-days: 1
@ -97,7 +97,7 @@ jobs:
- race - race
- sandbox - sandbox
- sandbox-race - sandbox-race
- hpkg - planterette
runs-on: nix runs-on: nix
steps: steps:
- name: Checkout - name: Checkout

View File

@ -1,5 +1 @@
DO NOT ADD NEW ACTIONS HERE This port is solely for releasing to the github mirror and serves no purpose during development.
This port is solely for releasing to the github mirror and serves no purpose during development.
All development happens at https://git.gensokyo.uk/security/hakurei. If you wish to contribute,
request for an account on git.gensokyo.uk.

5
.gitignore vendored
View File

@ -29,7 +29,4 @@ go.work.sum
/cmd/hakurei/LICENSE /cmd/hakurei/LICENSE
# release # release
/dist/hakurei-* /dist/hakurei-*
# interactive nixos vm
nixos.qcow2

View File

@ -16,8 +16,7 @@
</p> </p>
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel. Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
It implements the application container of [planterette (WIP)](https://git.gensokyo.uk/security/planterette), It also implements [planterette (WIP)](cmd/planterette), a self-contained Android-like package manager with modern security features.
a self-contained Android-like package manager with modern security features.
## NixOS Module usage ## NixOS Module usage

View File

@ -14,7 +14,6 @@ import (
"time" "time"
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/internal/app" "hakurei.app/internal/app"
@ -95,7 +94,7 @@ func buildCommand(out io.Writer) command.Command {
Gid: us, Gid: us,
Username: "chronos", Username: "chronos",
Name: "Hakurei Permissive Default", Name: "Hakurei Permissive Default",
HomeDir: container.FHSVarEmpty, HomeDir: "/var/empty",
} }
} else { } else {
passwd = u passwd = u
@ -115,29 +114,21 @@ 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
}
var e system.Enablement
if wayland { if wayland {
e |= system.EWayland config.Enablements |= system.EWayland
} }
if x11 { if x11 {
e |= system.EX11 config.Enablements |= system.EX11
} }
if dBus { if dBus {
e |= system.EDBus config.Enablements |= system.EDBus
} }
if pulse { if pulse {
e |= system.EPulse config.Enablements |= system.EPulse
} }
config.Enablements = hst.NewEnablements(e)
// parse D-Bus config file from flags if applicable // parse D-Bus config file from flags if applicable
if dBus { if dBus {
@ -221,7 +212,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.String()), psFlagShort, flagJSON) printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath), 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.String()) s := state.NewMulti(std.Paths().RunDirPath)
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,7 +12,6 @@ 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"
@ -23,9 +22,9 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
info := &hst.Info{Paths: std.Paths()} info := new(hst.Info)
// get hid by querying uid of identity 0 // get fid by querying uid of aid 0
if uid, err := std.Uid(0); err != nil { if uid, err := std.Uid(0); err != nil {
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:") hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
os.Exit(1) os.Exit(1)
@ -39,10 +38,6 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
} }
t.Printf("User:\t%d\n", info.User) t.Printf("User:\t%d\n", info.User)
t.Printf("TempDir:\t%s\n", info.TempDir)
t.Printf("SharePath:\t%s\n", info.SharePath)
t.Printf("RuntimePath:\t%s\n", info.RuntimePath)
t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
} }
func printShowInstance( func printShowInstance(
@ -78,17 +73,17 @@ func printShowInstance(
} else { } else {
t.Printf(" Identity:\t%d\n", config.Identity) t.Printf(" Identity:\t%d\n", config.Identity)
} }
t.Printf(" Enablements:\t%s\n", config.Enablements.Unwrap().String()) t.Printf(" Enablements:\t%s\n", config.Enablements.String())
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 != nil { if config.Data != "" {
t.Printf(" Data:\t%s\n", config.Data) t.Printf(" Data:\t%s\n", config.Data)
} }
if config.Container != nil { if config.Container != nil {
params := config.Container container := config.Container
if params.Hostname != "" { if container.Hostname != "" {
t.Printf(" Hostname:\t%s\n", params.Hostname) t.Printf(" Hostname:\t%s\n", container.Hostname)
} }
flags := make([]string, 0, 7) flags := make([]string, 0, 7)
writeFlag := func(name string, value bool) { writeFlag := func(name string, value bool) {
@ -96,32 +91,30 @@ func printShowInstance(
flags = append(flags, name) flags = append(flags, name)
} }
} }
writeFlag("userns", params.Userns) writeFlag("userns", container.Userns)
writeFlag("devel", params.Devel) writeFlag("devel", container.Devel)
writeFlag("net", params.Net) writeFlag("net", container.Net)
writeFlag("device", params.Device) writeFlag("device", container.Device)
writeFlag("tty", params.Tty) writeFlag("tty", container.Tty)
writeFlag("mapuid", params.MapRealUID) writeFlag("mapuid", container.MapRealUID)
writeFlag("directwl", config.DirectWayland) writeFlag("directwl", config.DirectWayland)
writeFlag("autoetc", params.AutoEtc) writeFlag("autoetc", container.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 params.AutoRoot != nil { etc := container.Etc
t.Printf(" Root:\t%s (%d)\n", params.AutoRoot, params.RootFlags) if etc == "" {
} etc = "/etc"
etc := params.Etc
if etc == nil {
etc = container.AbsFHSEtc
} }
t.Printf(" Etc:\t%s\n", etc) t.Printf(" Etc:\t%s\n", etc)
if config.Path != nil { if len(container.Cover) > 0 {
t.Printf(" Path:\t%s\n", config.Path) t.Printf(" Cover:\t%s\n", strings.Join(container.Cover, " "))
} }
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, " "))
@ -132,11 +125,30 @@ 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.Valid() { if f == nil {
t.Println(" <invalid>")
continue continue
} }
t.Printf(" %s\n", f)
expr := new(strings.Builder)
expr.Grow(3 + len(f.Src) + 1 + len(f.Dst))
if f.Device {
expr.WriteString(" d")
} else if f.Write {
expr.WriteString(" w")
} else {
expr.WriteString(" ")
}
if f.Must {
expr.WriteString("*")
} else {
expr.WriteString("+")
}
expr.WriteString(f.Src)
if f.Dst != "" {
expr.WriteString(":" + f.Dst)
}
t.Printf("%s\n", expr.String())
} }
t.Printf("\n") t.Printf("\n")
} }

View File

@ -42,17 +42,16 @@ func Test_printShowInstance(t *testing.T) {
Data: /var/lib/hakurei/u0/org.chromium.Chromium Data: /var/lib/hakurei/u0/org.chromium.Chromium
Hostname: localhost Hostname: localhost
Flags: userns devel net device tty mapuid autoetc Flags: userns devel net device tty mapuid autoetc
Root: /var/lib/hakurei/base/org.debian (2) Etc: /etc
Etc: /etc/ Cover: /var/run/nscd
Path: /run/current-system/sw/bin/chromium Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem Filesystem
w+ephemeral(-rwxr-xr-x):/tmp/ +/nix/store
w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store +/run/current-system
*/nix/store +/run/opengl-driver
*/run/current-system +/var/db/nix-channels
*/run/opengl-driver
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
d+/dev/dri d+/dev/dri
@ -83,17 +82,18 @@ 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.FilesystemConfigJSON, 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
@ -121,17 +121,16 @@ App
Data: /var/lib/hakurei/u0/org.chromium.Chromium Data: /var/lib/hakurei/u0/org.chromium.Chromium
Hostname: localhost Hostname: localhost
Flags: userns devel net device tty mapuid autoetc Flags: userns devel net device tty mapuid autoetc
Root: /var/lib/hakurei/base/org.debian (2) Etc: /etc
Etc: /etc/ Cover: /var/run/nscd
Path: /run/current-system/sw/bin/chromium Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem Filesystem
w+ephemeral(-rwxr-xr-x):/tmp/ +/nix/store
w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store +/run/current-system
*/nix/store +/run/opengl-driver
*/run/current-system +/var/db/nix-channels
*/run/opengl-driver
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
d+/dev/dri d+/dev/dri
@ -195,11 +194,7 @@ App
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
], ],
"enablements": { "enablements": 13,
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": { "session_bus": {
"see": null, "see": null,
"talk": [ "talk": [
@ -261,10 +256,8 @@ App
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
@ -279,55 +272,39 @@ App
"device": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"type": "ephemeral",
"dst": "/tmp/",
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store" "src": "/nix/store"
}, },
{ {
"type": "bind",
"src": "/run/current-system" "src": "/run/current-system"
}, },
{ {
"type": "bind",
"src": "/run/opengl-driver" "src": "/run/opengl-driver"
}, },
{ {
"type": "bind", "src": "/var/db/nix-channels"
"dst": "/data/data/org.chromium.Chromium", },
"src": "/var/lib/hakurei/u0/org.chromium.Chromium", {
"write": true "dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true,
"require": true
}, },
{ {
"type": "bind",
"src": "/dev/dri", "src": "/dev/dri",
"dev": true, "dev": true
"optional": true
} }
], ],
"symlink": [ "symlink": [
{ [
"target": "/run/user/65534", "/run/user/65534",
"linkname": "/run/user/150" "/run/user/150"
} ]
], ],
"auto_root": "/var/lib/hakurei/base/org.debian", "etc": "/etc",
"root_flags": 2, "auto_etc": true,
"etc": "/etc/", "cover": [
"auto_etc": true "/var/run/nscd"
]
} }
}, },
"time": "1970-01-01T00:00:00.000000009Z" "time": "1970-01-01T00:00:00.000000009Z"
@ -343,11 +320,7 @@ App
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
], ],
"enablements": { "enablements": 13,
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": { "session_bus": {
"see": null, "see": null,
"talk": [ "talk": [
@ -409,10 +382,8 @@ App
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
@ -427,55 +398,39 @@ App
"device": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"type": "ephemeral",
"dst": "/tmp/",
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store" "src": "/nix/store"
}, },
{ {
"type": "bind",
"src": "/run/current-system" "src": "/run/current-system"
}, },
{ {
"type": "bind",
"src": "/run/opengl-driver" "src": "/run/opengl-driver"
}, },
{ {
"type": "bind", "src": "/var/db/nix-channels"
"dst": "/data/data/org.chromium.Chromium", },
"src": "/var/lib/hakurei/u0/org.chromium.Chromium", {
"write": true "dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true,
"require": true
}, },
{ {
"type": "bind",
"src": "/dev/dri", "src": "/dev/dri",
"dev": true, "dev": true
"optional": true
} }
], ],
"symlink": [ "symlink": [
{ [
"target": "/run/user/65534", "/run/user/65534",
"linkname": "/run/user/150" "/run/user/150"
} ]
], ],
"auto_root": "/var/lib/hakurei/base/org.debian", "etc": "/etc",
"root_flags": 2, "auto_etc": true,
"etc": "/etc/", "cover": [
"auto_etc": true "/var/run/nscd"
]
} }
} }
`}, `},
@ -545,11 +500,7 @@ func Test_printPs(t *testing.T) {
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
], ],
"enablements": { "enablements": 13,
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": { "session_bus": {
"see": null, "see": null,
"talk": [ "talk": [
@ -611,10 +562,8 @@ func Test_printPs(t *testing.T) {
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
@ -629,55 +578,39 @@ func Test_printPs(t *testing.T) {
"device": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"type": "ephemeral",
"dst": "/tmp/",
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store" "src": "/nix/store"
}, },
{ {
"type": "bind",
"src": "/run/current-system" "src": "/run/current-system"
}, },
{ {
"type": "bind",
"src": "/run/opengl-driver" "src": "/run/opengl-driver"
}, },
{ {
"type": "bind", "src": "/var/db/nix-channels"
"dst": "/data/data/org.chromium.Chromium", },
"src": "/var/lib/hakurei/u0/org.chromium.Chromium", {
"write": true "dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true,
"require": true
}, },
{ {
"type": "bind",
"src": "/dev/dri", "src": "/dev/dri",
"dev": true, "dev": true
"optional": true
} }
], ],
"symlink": [ "symlink": [
{ [
"target": "/run/user/65534", "/run/user/65534",
"linkname": "/run/user/150" "/run/user/150"
} ]
], ],
"auto_root": "/var/lib/hakurei/base/org.debian", "etc": "/etc",
"root_flags": 2, "auto_etc": true,
"etc": "/etc/", "cover": [
"auto_etc": true "/var/run/nscd"
]
} }
}, },
"time": "1970-01-01T00:00:00.000000009Z" "time": "1970-01-01T00:00:00.000000009Z"

View File

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

View File

@ -4,10 +4,11 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"os" "os"
"path"
"hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/system"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -42,7 +43,7 @@ type appInfo struct {
// passed through to [hst.Config] // passed through to [hst.Config]
SessionBus *dbus.Config `json:"session_bus,omitempty"` SessionBus *dbus.Config `json:"session_bus,omitempty"`
// passed through to [hst.Config] // passed through to [hst.Config]
Enablements *hst.Enablements `json:"enablements,omitempty"` Enablements system.Enablement `json:"enablements"`
// passed through to [hst.Config] // passed through to [hst.Config]
Multiarch bool `json:"multiarch,omitempty"` Multiarch bool `json:"multiarch,omitempty"`
@ -56,18 +57,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 *container.Absolute `json:"launcher"` Launcher string `json:"launcher"`
// store path to /run/current-system // store path to /run/current-system
CurrentSystem *container.Absolute `json:"current_system"` CurrentSystem string `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) toHst(pathSet *appPathSet, pathname *container.Absolute, argv []string, flagDropShell bool) *hst.Config { func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *hst.Config {
config := &hst.Config{ config := &hst.Config{
ID: app.ID, ID: app.ID,
Path: pathname, Path: argv[0],
Args: argv, Args: argv,
Enablements: app.Enablements, Enablements: app.Enablements,
@ -77,9 +78,9 @@ func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, arg
DirectWayland: app.DirectWayland, DirectWayland: app.DirectWayland,
Username: "hakurei", Username: "hakurei",
Shell: pathShell, Shell: shellPath,
Data: pathSet.homeDir, Data: pathSet.homeDir,
Dir: pathDataData.Append(app.ID), Dir: path.Join("/data/data", app.ID),
Identity: app.Identity, Identity: app.Identity,
Groups: app.Groups, Groups: app.Groups,
@ -92,22 +93,22 @@ func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, arg
Device: app.Device, Device: app.Device,
Tty: app.Tty || flagDropShell, Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID, MapRealUID: app.MapRealUID,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []*hst.FilesystemConfig{
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}}, {Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsTmp.Append("app")}}, {Src: pathSet.metaPath, Dst: path.Join(hst.Tmp, "app"), Must: true},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}}, {Src: "/etc/resolv.conf"},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}}, {Src: "/sys/block"},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}}, {Src: "/sys/bus"},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}}, {Src: "/sys/class"},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}}, {Src: "/sys/dev"},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}}, {Src: "/sys/devices"},
}, },
Link: []hst.LinkConfig{ Link: [][2]string{
{pathCurrentSystem, app.CurrentSystem.String()}, {app.CurrentSystem, "/run/current-system"},
{pathBin, pathSwBin.String()}, {"/run/current-system/sw/bin", "/bin"},
{container.AbsFHSUsrBin, pathSwBin.String()}, {"/run/current-system/sw/bin", "/usr/bin"},
}, },
Etc: pathSet.cacheDir.Append("etc"), Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true, AutoEtc: true,
}, },
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []*hst.ExtraPermConfig{
@ -141,14 +142,6 @@ 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

@ -171,12 +171,7 @@ let
broadcast = { }; broadcast = { };
}); });
enablements = { enablements = (if allow_wayland then 1 else 0) + (if allow_x11 then 2 else 0) + (if allow_dbus then 4 else 0) + (if allow_pulse then 8 else 0);
wayland = allow_wayland;
x11 = allow_x11;
dbus = allow_dbus;
pulse = allow_pulse;
};
mesa = if gpu then mesaWrappers else null; mesa = if gpu then mesaWrappers else null;
nix_gl = if gpu then nixGL else null; nix_gl = if gpu then nixGL else null;
@ -220,14 +215,15 @@ stdenv.mkDerivation {
# create binary cache # create binary cache
closureInfo="${ closureInfo="${
closureInfo { closureInfo {
rootPaths = [ rootPaths =
homeManagerConfiguration.activationPackage [
launcher homeManagerConfiguration.activationPackage
] launcher
++ optionals gpu [ ]
mesaWrappers ++ optionals gpu [
nixGL mesaWrappers
]; nixGL
];
} }
}" }"
echo "copying application paths..." echo "copying application paths..."

View File

@ -11,19 +11,20 @@ import (
"syscall" "syscall"
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
"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("planterette")
if err := os.Setenv("SHELL", pathShell.String()); err != nil { if err := os.Setenv("SHELL", shellPath); err != nil {
log.Fatalf("cannot set $SHELL: %v", err) log.Fatalf("cannot set $SHELL: %v", err)
} }
} }
@ -41,7 +42,7 @@ func main() {
flagVerbose bool flagVerbose bool
flagDropShell bool flagDropShell bool
) )
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { internal.InstallOutput(flagVerbose); return nil }). c := command.New(os.Stderr, log.Printf, "planterette", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console"). Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action") Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
@ -65,7 +66,7 @@ func main() {
} }
/* /*
Look up paths to programs started by hpkg. Look up paths to programs started by planterette.
This is done here to ease error handling as cleanup is not yet required. This is done here to ease error handling as cleanup is not yet required.
*/ */
@ -80,32 +81,31 @@ func main() {
Extract package and set up for cleanup. Extract package and set up for cleanup.
*/ */
var workDir *container.Absolute var workDir string
if p, err := os.MkdirTemp("", "hpkg.*"); err != nil { if p, err := os.MkdirTemp("", "planterette.*"); err != nil {
log.Printf("cannot create temporary directory: %v", err) log.Printf("cannot create temporary directory: %v", err)
return err return err
} else if workDir, err = container.NewAbs(p); err != nil { } else {
log.Printf("invalid temporary directory: %v", err) workDir = p
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.String()) mustRun(chmod, "-R", "+w", workDir)
mustRun(rm, "-rf", workDir.String()) mustRun(rm, "-rf", workDir)
} }
beforeRunFail.Store(&cleanup) beforeRunFail.Store(&cleanup)
mustRun(tar, "-C", workDir.String(), "-xf", pkgPath) mustRun(tar, "-C", workDir, "-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.String(), "bundle.json"), cleanup) bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup)
pathSet := pathSetByApp(bundle.ID) pathSet := pathSetByApp(bundle.ID)
a := bundle a := bundle
if s, err := os.Stat(pathSet.metaPath.String()); err != nil { if s, err := os.Stat(pathSet.metaPath); 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)
@ -117,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.String(), cleanup) a = loadAppInfo(pathSet.metaPath, 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",
@ -208,7 +208,7 @@ func main() {
*/ */
// serialise metadata to ensure consistency // serialise metadata to ensure consistency
if f, err := os.OpenFile(pathSet.metaPath.String()+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil { if f, err := os.OpenFile(pathSet.metaPath+"~", 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
@ -221,7 +221,7 @@ func main() {
// not fatal // not fatal
} }
if err := os.Rename(pathSet.metaPath.String()+"~", pathSet.metaPath.String()); err != nil { if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
cleanup() cleanup()
log.Printf("cannot rename metadata file: %v", err) log.Printf("cannot rename metadata file: %v", err)
return err return err
@ -250,7 +250,7 @@ func main() {
id := args[0] id := args[0]
pathSet := pathSetByApp(id) pathSet := pathSetByApp(id)
a := loadAppInfo(pathSet.metaPath.String(), func() {}) a := loadAppInfo(pathSet.metaPath, 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
@ -274,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.FilesystemConfigJSON{ config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}}, {Src: "/etc/resolv.conf"},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}}, {Src: "/sys/block"},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}}, {Src: "/sys/bus"},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}}, {Src: "/sys/class"},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}}, {Src: "/sys/dev"},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}}, {Src: "/sys/devices"},
}...) }...)
appendGPUFilesystem(config) appendGPUFilesystem(config)
return config return config
@ -291,16 +291,15 @@ func main() {
Create app configuration. Create app configuration.
*/ */
pathname := a.Launcher
argv := make([]string, 1, len(args)) argv := make([]string, 1, len(args))
if flagDropShell { if !flagDropShell {
pathname = pathShell argv[0] = a.Launcher
argv[0] = bash
} else { } else {
argv[0] = a.Launcher.String() argv[0] = shellPath
} }
argv = append(argv, args[1:]...) argv = append(argv, args[1:]...)
config := a.toHst(pathSet, pathname, argv, flagDropShell)
config := a.toFst(pathSet, argv, flagDropShell)
/* /*
Expose GPU devices. Expose GPU devices.
@ -308,7 +307,7 @@ func main() {
if a.GPU { if a.GPU {
config.Container.Filesystem = append(config.Container.Filesystem, config.Container.Filesystem = append(config.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsTmp.Append("nixGL")}}) &hst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(hst.Tmp, "nixGL")})
appendGPUFilesystem(config) appendGPUFilesystem(config)
} }

101
cmd/planterette/paths.go Normal file
View File

@ -0,0 +1,101 @@
package main
import (
"log"
"os"
"os/exec"
"path"
"strconv"
"sync/atomic"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
var (
dataHome string
)
func init() {
// dataHome
if p, ok := os.LookupEnv("HAKUREI_DATA_HOME"); ok {
dataHome = p
} else {
dataHome = "/var/lib/hakurei/" + strconv.Itoa(os.Getuid())
}
}
func lookPath(file string) string {
if p, err := exec.LookPath(file); err != nil {
log.Fatalf("%s: command not found", file)
return ""
} else {
return p
}
}
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(name string, arg ...string) {
hlog.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
if f := beforeRunFail.Swap(nil); f != nil {
(*f)()
}
log.Fatalf("%s: %v", name, err)
}
}
type appPathSet struct {
// ${dataHome}/${id}
baseDir string
// ${baseDir}/app
metaPath string
// ${baseDir}/files
homeDir string
// ${baseDir}/cache
cacheDir string
// ${baseDir}/cache/nix
nixPath string
}
func pathSetByApp(id string) *appPathSet {
pathSet := new(appPathSet)
pathSet.baseDir = path.Join(dataHome, id)
pathSet.metaPath = path.Join(pathSet.baseDir, "app")
pathSet.homeDir = path.Join(pathSet.baseDir, "files")
pathSet.cacheDir = path.Join(pathSet.baseDir, "cache")
pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
return pathSet
}
func appendGPUFilesystem(config *hst.Config) {
config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{Src: "/dev/dri", Device: true},
// mali
{Src: "/dev/mali", Device: true},
{Src: "/dev/mali0", Device: true},
{Src: "/dev/umplock", Device: true},
// nvidia
{Src: "/dev/nvidiactl", Device: true},
{Src: "/dev/nvidia-modeset", Device: true},
// nvidia OpenCL/CUDA
{Src: "/dev/nvidia-uvm", Device: true},
{Src: "/dev/nvidia-uvm-tools", Device: true},
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
{Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true},
{Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true},
{Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true},
{Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true},
{Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true},
{Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true},
{Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true},
{Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true},
{Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true},
{Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true},
}...)
}

View File

@ -9,7 +9,7 @@ let
buildPackage = self.buildPackage.${system}; buildPackage = self.buildPackage.${system};
in in
nixosTest { nixosTest {
name = "hpkg"; name = "planterette";
nodes.machine = { nodes.machine = {
environment.etc = { environment.etc = {
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; }; "foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };

View File

@ -79,20 +79,20 @@ print(machine.succeed("sudo -u alice -i hakurei version"))
machine.wait_for_file("/run/user/1000/wayland-1") machine.wait_for_file("/run/user/1000/wayland-1")
machine.wait_for_file("/tmp/sway-ipc.sock") machine.wait_for_file("/tmp/sway-ipc.sock")
# Prepare hpkg directory: # Prepare planterette directory:
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000") machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
# Install hpkg app: # Install planterette app:
swaymsg("exec hpkg -v install /etc/foot.pkg && touch /tmp/hpkg-install-ok") swaymsg("exec planterette -v install /etc/foot.pkg && touch /tmp/planterette-install-ok")
machine.wait_for_file("/tmp/hpkg-install-ok") machine.wait_for_file("/tmp/planterette-install-ok")
# Start app (foot) with Wayland enablement: # Start app (foot) with Wayland enablement:
swaymsg("exec hpkg -v start org.codeberg.dnkl.foot") swaymsg("exec planterette -v start org.codeberg.dnkl.foot")
wait_for_window("hakurei@machine-foot") wait_for_window("hakurei@machine-foot")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n") machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/hakurei.1000/tmpdir/2/success-client") machine.wait_for_file("/tmp/hakurei.1000/tmpdir/2/success-client")
collect_state_ui("app_wayland") collect_state_ui("app_wayland")
check_state("foot", {"wayland": True, "dbus": True, "pulse": True}) check_state("foot", 13)
# Verify acl on XDG_RUNTIME_DIR: # Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")) print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
machine.send_chars("exit\n") machine.send_chars("exit\n")

View File

@ -2,9 +2,9 @@ package main
import ( import (
"context" "context"
"path"
"strings" "strings"
"hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
@ -18,8 +18,8 @@ func withNixDaemon(
mustRunAppDropShell(ctx, updateConfig(&hst.Config{ mustRunAppDropShell(ctx, updateConfig(&hst.Config{
ID: app.ID, ID: app.ID,
Path: pathShell, Path: shellPath,
Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " + Args: []string{shellPath, "-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
@ -32,9 +32,9 @@ func withNixDaemon(
}, },
Username: "hakurei", Username: "hakurei",
Shell: pathShell, Shell: shellPath,
Data: pathSet.homeDir, Data: pathSet.homeDir,
Dir: pathDataData.Append(app.ID), Dir: path.Join("/data/data", 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},
@ -48,15 +48,15 @@ func withNixDaemon(
Net: net, Net: net,
SeccompFlags: seccomp.AllowMultiarch, SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell, Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []*hst.FilesystemConfig{
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}}, {Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
}, },
Link: []hst.LinkConfig{ Link: [][2]string{
{pathCurrentSystem, app.CurrentSystem.String()}, {app.CurrentSystem, "/run/current-system"},
{pathBin, pathSwBin.String()}, {"/run/current-system/sw/bin", "/bin"},
{container.AbsFHSUsrBin, pathSwBin.String()}, {"/run/current-system/sw/bin", "/usr/bin"},
}, },
Etc: pathSet.cacheDir.Append("etc"), Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true, AutoEtc: true,
}, },
}), dropShell, beforeFail) }), dropShell, beforeFail)
@ -64,18 +64,18 @@ func withNixDaemon(
func withCacheDir( func withCacheDir(
ctx context.Context, ctx context.Context,
action string, command []string, workDir *container.Absolute, action string, command []string, workDir string,
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: pathShell, Path: shellPath,
Args: []string{bash, "-lc", strings.Join(command, " && ")}, Args: []string{shellPath, "-lc", strings.Join(command, " && ")},
Username: "nixos", Username: "nixos",
Shell: pathShell, Shell: shellPath,
Data: pathSet.cacheDir, // this also ensures cacheDir via shim Data: pathSet.cacheDir, // this also ensures cacheDir via shim
Dir: pathDataData.Append(app.ID, "cache"), Dir: path.Join("/data/data", 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},
@ -88,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.FilesystemConfigJSON{ Filesystem: []*hst.FilesystemConfig{
{FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}}, {Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}}, {Src: workDir, Dst: path.Join(hst.Tmp, "bundle"), Must: true},
}, },
Link: []hst.LinkConfig{ Link: [][2]string{
{pathCurrentSystem, app.CurrentSystem.String()}, {app.CurrentSystem, "/run/current-system"},
{pathBin, pathSwBin.String()}, {"/run/current-system/sw/bin", "/bin"},
{container.AbsFHSUsrBin, pathSwBin.String()}, {"/run/current-system/sw/bin", "/usr/bin"},
}, },
Etc: workDir.Append(container.FHSEtc), Etc: path.Join(workDir, "etc"),
AutoEtc: true, AutoEtc: true,
}, },
}, dropShell, beforeFail) }, dropShell, beforeFail)
@ -105,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{bash, "-l"} config.Args = []string{shellPath, "-l"}
mustRunApp(ctx, config, beforeFail) mustRunApp(ctx, config, beforeFail)
beforeFail() beforeFail()
internal.Exit(0) internal.Exit(0)

View File

@ -1,98 +0,0 @@
package container
import (
"encoding/json"
"errors"
"fmt"
"path"
"slices"
"strings"
"syscall"
)
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
type AbsoluteError struct {
Pathname string
}
func (e *AbsoluteError) Error() string { return fmt.Sprintf("path %q is not absolute", e.Pathname) }
func (e *AbsoluteError) Is(target error) bool {
var ce *AbsoluteError
if !errors.As(target, &ce) {
return errors.Is(target, syscall.EINVAL)
}
return *e == *ce
}
// Absolute holds a pathname checked to be absolute.
type Absolute struct {
pathname string
}
// isAbs wraps [path.IsAbs] in case additional checks are added in the future.
func isAbs(pathname string) bool { return path.IsAbs(pathname) }
func (a *Absolute) String() string {
if a.pathname == zeroString {
panic("attempted use of zero Absolute")
}
return a.pathname
}
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
func NewAbs(pathname string) (*Absolute, error) {
if !isAbs(pathname) {
return nil, &AbsoluteError{pathname}
}
return &Absolute{pathname}, nil
}
// MustAbs calls [NewAbs] and panics on error.
func MustAbs(pathname string) *Absolute {
if a, err := NewAbs(pathname); err != nil {
panic(err.Error())
} else {
return a
}
}
// Append calls [path.Join] with [Absolute] as the first element.
func (a *Absolute) Append(elem ...string) *Absolute {
return &Absolute{path.Join(append([]string{a.String()}, elem...)...)}
}
// Dir calls [path.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return &Absolute{path.Dir(a.String())} }
func (a *Absolute) GobEncode() ([]byte, error) { return []byte(a.String()), nil }
func (a *Absolute) GobDecode(data []byte) error {
pathname := string(data)
if !isAbs(pathname) {
return &AbsoluteError{pathname}
}
a.pathname = pathname
return nil
}
func (a *Absolute) MarshalJSON() ([]byte, error) { return json.Marshal(a.String()) }
func (a *Absolute) UnmarshalJSON(data []byte) error {
var pathname string
if err := json.Unmarshal(data, &pathname); err != nil {
return err
}
if !isAbs(pathname) {
return &AbsoluteError{pathname}
}
a.pathname = pathname
return nil
}
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
func SortAbs(x []*Absolute) {
slices.SortFunc(x, func(a, b *Absolute) int { return strings.Compare(a.String(), b.String()) })
}
// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
func CompactAbs(s []*Absolute) []*Absolute {
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool { return a.String() == b.String() })
}

View File

@ -1,325 +0,0 @@
package container
import (
"bytes"
"encoding/gob"
"encoding/json"
"errors"
"reflect"
"strings"
"syscall"
"testing"
)
func TestAbsoluteError(t *testing.T) {
testCases := []struct {
name string
err error
cmp error
ok bool
}{
{"EINVAL", new(AbsoluteError), syscall.EINVAL, true},
{"not EINVAL", new(AbsoluteError), syscall.EBADE, false},
{"ne val", new(AbsoluteError), &AbsoluteError{"etc"}, false},
{"equals", &AbsoluteError{"etc"}, &AbsoluteError{"etc"}, true},
}
for _, tc := range testCases {
if got := errors.Is(tc.err, tc.cmp); got != tc.ok {
t.Errorf("Is: %v, want %v", got, tc.ok)
}
}
t.Run("string", func(t *testing.T) {
want := `path "etc" is not absolute`
if got := (&AbsoluteError{"etc"}).Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
}
func TestNewAbs(t *testing.T) {
testCases := []struct {
name string
pathname string
want *Absolute
wantErr error
}{
{"good", "/etc", MustAbs("/etc"), nil},
{"not absolute", "etc", nil, &AbsoluteError{"etc"}},
{"zero", "", nil, &AbsoluteError{""}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := NewAbs(tc.pathname)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("NewAbs: %#v, want %#v", got, tc.want)
}
if !errors.Is(err, tc.wantErr) {
t.Errorf("NewAbs: error = %v, want %v", err, tc.wantErr)
}
})
}
t.Run("must", func(t *testing.T) {
defer func() {
wantPanic := `path "etc" is not absolute`
if r := recover(); r != wantPanic {
t.Errorf("MustAbsolute: panic = %v; want %v", r, wantPanic)
}
}()
MustAbs("etc")
})
}
func TestAbsoluteString(t *testing.T) {
t.Run("passthrough", func(t *testing.T) {
pathname := "/etc"
if got := (&Absolute{pathname}).String(); got != pathname {
t.Errorf("String: %q, want %q", got, pathname)
}
})
t.Run("zero", func(t *testing.T) {
defer func() {
wantPanic := "attempted use of zero Absolute"
if r := recover(); r != wantPanic {
t.Errorf("String: panic = %v, want %v", r, wantPanic)
}
}()
panic(new(Absolute).String())
})
}
type sCheck struct {
Pathname *Absolute `json:"val"`
Magic int `json:"magic"`
}
func TestCodecAbsolute(t *testing.T) {
testCases := []struct {
name string
a *Absolute
wantErr error
gob, sGob string
json, sJson string
}{
{"nil", nil, nil,
"\x00", "\x00",
`null`, `{"val":null,"magic":3236757504}`},
{"good", MustAbs("/etc"),
nil,
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x10\xff\x84\x01\x04/etc\x01\xfb\x01\x81\xda\x00\x00\x00",
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
{"not absolute", nil,
&AbsoluteError{"etc"},
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
`"etc"`, `{"val":"etc","magic":3236757504}`},
{"zero", nil,
new(AbsoluteError),
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
`""`, `{"val":"","magic":3236757504}`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("gob", func(t *testing.T) {
if tc.gob == "\x00" && tc.sGob == "\x00" {
// these values mark the current test to skip gob
return
}
t.Run("encode", func(t *testing.T) {
// encode is unchecked
if errors.Is(tc.wantErr, syscall.EINVAL) {
return
}
{
buf := new(bytes.Buffer)
err := gob.NewEncoder(buf).Encode(tc.a)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Encode: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
goto checkSEncode
}
if buf.String() != tc.gob {
t.Errorf("Encode:\n%q\nwant:\n%q", buf.String(), tc.gob)
}
}
checkSEncode:
{
buf := new(bytes.Buffer)
err := gob.NewEncoder(buf).Encode(&sCheck{tc.a, syscall.MS_MGC_VAL})
if !errors.Is(err, tc.wantErr) {
t.Errorf("Encode: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
return
}
if buf.String() != tc.sGob {
t.Errorf("Encode:\n%q\nwant:\n%q", buf.String(), tc.sGob)
}
}
})
t.Run("decode", func(t *testing.T) {
{
var gotA *Absolute
err := gob.NewDecoder(strings.NewReader(tc.gob)).Decode(&gotA)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Decode: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
goto checkSDecode
}
if !reflect.DeepEqual(tc.a, gotA) {
t.Errorf("Decode: %#v, want %#v", tc.a, gotA)
}
}
checkSDecode:
{
var gotSCheck sCheck
err := gob.NewDecoder(strings.NewReader(tc.sGob)).Decode(&gotSCheck)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Decode: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
return
}
want := sCheck{tc.a, syscall.MS_MGC_VAL}
if !reflect.DeepEqual(gotSCheck, want) {
t.Errorf("Decode: %#v, want %#v", gotSCheck, want)
}
}
})
})
t.Run("json", func(t *testing.T) {
t.Run("marshal", func(t *testing.T) {
// marshal is unchecked
if errors.Is(tc.wantErr, syscall.EINVAL) {
return
}
{
d, err := json.Marshal(tc.a)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Marshal: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
goto checkSMarshal
}
if string(d) != tc.json {
t.Errorf("Marshal:\n%s\nwant:\n%s", string(d), tc.json)
}
}
checkSMarshal:
{
d, err := json.Marshal(&sCheck{tc.a, syscall.MS_MGC_VAL})
if !errors.Is(err, tc.wantErr) {
t.Errorf("Marshal: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
return
}
if string(d) != tc.sJson {
t.Errorf("Marshal:\n%s\nwant:\n%s", string(d), tc.sJson)
}
}
})
t.Run("unmarshal", func(t *testing.T) {
{
var gotA *Absolute
err := json.Unmarshal([]byte(tc.json), &gotA)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Unmarshal: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
goto checkSUnmarshal
}
if !reflect.DeepEqual(tc.a, gotA) {
t.Errorf("Unmarshal: %#v, want %#v", tc.a, gotA)
}
}
checkSUnmarshal:
{
var gotSCheck sCheck
err := json.Unmarshal([]byte(tc.sJson), &gotSCheck)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Unmarshal: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
return
}
want := sCheck{tc.a, syscall.MS_MGC_VAL}
if !reflect.DeepEqual(gotSCheck, want) {
t.Errorf("Unmarshal: %#v, want %#v", gotSCheck, want)
}
}
})
})
})
}
t.Run("json passthrough", func(t *testing.T) {
wantErr := "invalid character ':' looking for beginning of value"
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
}
})
}
func TestAbsoluteWrap(t *testing.T) {
t.Run("join", func(t *testing.T) {
want := "/etc/nix/nix.conf"
if got := MustAbs("/etc").Append("nix", "nix.conf"); got.String() != want {
t.Errorf("Append: %q, want %q", got, want)
}
})
t.Run("dir", func(t *testing.T) {
want := "/"
if got := MustAbs("/etc").Dir(); got.String() != want {
t.Errorf("Dir: %q, want %q", got, want)
}
})
t.Run("sort", func(t *testing.T) {
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
got := []*Absolute{MustAbs("/proc"), MustAbs("/sys"), MustAbs("/etc")}
SortAbs(got)
if !reflect.DeepEqual(got, want) {
t.Errorf("SortAbs: %#v, want %#v", got, want)
}
})
t.Run("compact", func(t *testing.T) {
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
if got := CompactAbs([]*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/proc"), MustAbs("/sys")}); !reflect.DeepEqual(got, want) {
t.Errorf("CompactAbs: %#v, want %#v", got, want)
}
})
}

View File

@ -1,73 +0,0 @@
package container
import (
"encoding/gob"
"fmt"
"os"
"syscall"
)
func init() { gob.Register(new(AutoEtcOp)) }
// Etc appends an [Op] that expands host /etc into a toplevel symlink mirror with /etc semantics.
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Etc(host *Absolute, prefix string) *Ops {
e := &AutoEtcOp{prefix}
f.Mkdir(AbsFHSEtc, 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)
return f
}
type AutoEtcOp struct{ Prefix string }
func (e *AutoEtcOp) early(*setupState) error { return nil }
func (e *AutoEtcOp) apply(state *setupState) error {
if state.nonrepeatable&nrAutoEtc != 0 {
return msg.WrapErr(syscall.EINVAL, "autoetc is not repeatable")
}
state.nonrepeatable |= nrAutoEtc
const target = sysrootPath + FHSEtc
rel := e.hostRel() + "/"
if err := os.MkdirAll(target, 0755); err != nil {
return wrapErrSelf(err)
}
if d, err := os.ReadDir(toSysroot(e.hostPath().String())); err != nil {
return wrapErrSelf(err)
} else {
for _, ent := range d {
n := ent.Name()
switch n {
case ".host":
case "passwd":
case "group":
case "mtab":
if err = os.Symlink(FHSProc+"mounts", target+n); err != nil {
return wrapErrSelf(err)
}
default:
if err = os.Symlink(rel+n, target+n); err != nil {
return wrapErrSelf(err)
}
}
}
}
return nil
}
// bypasses abs check, use with caution!
func (e *AutoEtcOp) hostPath() *Absolute { return &Absolute{FHSEtc + e.hostRel()} }
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtcOp) Is(op Op) bool {
ve, ok := op.(*AutoEtcOp)
return ok && ((e == nil && ve == nil) || (e != nil && ve != nil && *e == *ve))
}
func (*AutoEtcOp) prefix() string { return "setting up" }
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }

View File

@ -1,96 +0,0 @@
package container
import (
"encoding/gob"
"fmt"
"os"
"syscall"
)
func init() { gob.Register(new(AutoRootOp)) }
// Root appends an [Op] that expands a directory into a toplevel bind mount mirror on container root.
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Root(host *Absolute, prefix string, flags int) *Ops {
*f = append(*f, &AutoRootOp{host, prefix, flags, nil})
return f
}
type AutoRootOp struct {
Host *Absolute
Prefix string
// passed through to bindMount
Flags int
// obtained during early;
// these wrap the underlying Op because BindMountOp is relatively complex,
// so duplicating that code would be unwise
resolved []Op
}
func (r *AutoRootOp) early(state *setupState) error {
if r.Host == nil {
return syscall.EBADE
}
if d, err := os.ReadDir(r.Host.String()); err != nil {
return wrapErrSelf(err)
} else {
r.resolved = make([]Op, 0, len(d))
for _, ent := range d {
name := ent.Name()
if IsAutoRootBindable(name) {
op := &BindMountOp{
Source: r.Host.Append(name),
Target: AbsFHSRoot.Append(name),
Flags: r.Flags,
}
if err = op.early(state); err != nil {
return err
}
r.resolved = append(r.resolved, op)
}
}
return nil
}
}
func (r *AutoRootOp) apply(state *setupState) error {
if state.nonrepeatable&nrAutoRoot != 0 {
return msg.WrapErr(syscall.EINVAL, "autoroot is not repeatable")
}
state.nonrepeatable |= nrAutoRoot
for _, op := range r.resolved {
msg.Verbosef("%s %s", op.prefix(), op)
if err := op.apply(state); err != nil {
return err
}
}
return nil
}
func (r *AutoRootOp) Is(op Op) bool {
vr, ok := op.(*AutoRootOp)
return ok && ((r == nil && vr == nil) || (r != nil && vr != nil &&
r.Host == vr.Host && r.Prefix == vr.Prefix && r.Flags == vr.Flags))
}
func (*AutoRootOp) prefix() string { return "setting up" }
func (r *AutoRootOp) String() string {
return fmt.Sprintf("auto root %q prefix %s flags %#x", r.Host, r.Prefix, r.Flags)
}
// IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot.
func IsAutoRootBindable(name string) bool {
switch name {
case "proc":
case "dev":
case "tmp":
case "mnt":
case "etc":
default:
return true
}
return false
}

View File

@ -12,9 +12,8 @@ const (
PR_CAP_AMBIENT_RAISE = 0x2 PR_CAP_AMBIENT_RAISE = 0x2
PR_CAP_AMBIENT_CLEAR_ALL = 0x4 PR_CAP_AMBIENT_CLEAR_ALL = 0x4
CAP_SYS_ADMIN = 0x15 CAP_SYS_ADMIN = 0x15
CAP_SETPCAP = 0x8 CAP_SETPCAP = 0x8
CAP_DAC_OVERRIDE = 0x1
) )
type ( type (

View File

@ -9,6 +9,7 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path"
"strconv" "strconv"
. "syscall" . "syscall"
"time" "time"
@ -16,22 +17,21 @@ import (
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
) )
const (
// CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGTERM
)
type ( type (
// Container represents a container environment being prepared or run. // Container represents a container environment being prepared or run.
// None of [Container] methods are safe for concurrent use. // None of [Container] methods are safe for concurrent use.
Container struct { Container struct {
// Name of initial process in the container.
name string
// Cgroup fd, nil to disable. // Cgroup fd, nil to disable.
Cgroup *int Cgroup *int
// ExtraFiles passed through to initial process in the container, // ExtraFiles passed through to initial process in the container,
// with behaviour identical to its [exec.Cmd] counterpart. // with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File ExtraFiles []*os.File
// Custom [exec.Cmd] initialisation function.
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
// param encoder for shim and init // param encoder for shim and init
setup *gob.Encoder setup *gob.Encoder
// cancels cmd // cancels cmd
@ -52,17 +52,13 @@ 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 *Absolute Dir string
// Initial process environment. // Initial process environment.
Env []string Env []string
// Pathname of initial process in the container. // Absolute path of initial process in the container. Overrides name.
Path *Absolute Path string
// Initial process argv. // Initial process argv.
Args []string Args []string
// Deliver SIGINT to the initial process on context cancellation.
ForwardCancel bool
// time to wait for linger processes after death of initial process
AdoptWaitDelay time.Duration
// Mapped Uid in user namespace. // Mapped Uid in user namespace.
Uid int Uid int
@ -72,7 +68,6 @@ type (
Hostname string Hostname string
// Sequential container setup ops. // Sequential container setup ops.
*Ops *Ops
// Seccomp system call filter rules. // Seccomp system call filter rules.
SeccompRules []seccomp.NativeRule SeccompRules []seccomp.NativeRule
// Extra seccomp flags. // Extra seccomp flags.
@ -81,7 +76,6 @@ type (
SeccompPresets seccomp.FilterPreset SeccompPresets seccomp.FilterPreset
// Do not load seccomp program. // Do not load seccomp program.
SeccompDisable bool SeccompDisable bool
// Permission bits of newly created parent directories. // Permission bits of newly created parent directories.
// The zero value is interpreted as 0755. // The zero value is interpreted as 0755.
ParentPerm os.FileMode ParentPerm os.FileMode
@ -96,18 +90,22 @@ type (
} }
) )
// Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error { func (p *Container) Start() error {
if p.cmd != nil { if p.cmd != nil {
return errors.New("container: already started") return errors.New("sandbox: already started")
} }
if p.Ops == nil || len(*p.Ops) == 0 { if p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("container: starting an empty container") return errors.New("sandbox: starting an empty container")
} }
ctx, cancel := context.WithCancel(p.ctx) ctx, cancel := context.WithCancel(p.ctx)
p.cancel = cancel p.cancel = cancel
var cloneFlags uintptr = CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP
if !p.HostNet {
cloneFlags |= CLONE_NEWNET
}
// map to overflow id to work around ownership checks // map to overflow id to work around ownership checks
if p.Uid < 1 { if p.Uid < 1 {
p.Uid = OverflowUid() p.Uid = OverflowUid()
@ -120,47 +118,34 @@ func (p *Container) Start() error {
p.SeccompPresets |= seccomp.PresetDenyTTY p.SeccompPresets |= seccomp.PresetDenyTTY
} }
if p.AdoptWaitDelay == 0 { if p.CommandContext != nil {
p.AdoptWaitDelay = 5 * time.Second p.cmd = p.CommandContext(ctx)
} } else {
// to allow disabling this behaviour p.cmd = exec.CommandContext(ctx, MustExecutable())
if p.AdoptWaitDelay < 0 { p.cmd.Args = []string{"init"}
p.AdoptWaitDelay = 0
} }
p.cmd = exec.CommandContext(ctx, MustExecutable())
p.cmd.Args = []string{initName}
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil { if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) } p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
} else { } else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) } p.cmd.Cancel = func() error { return p.cmd.Process.Signal(SIGTERM) }
} }
p.cmd.Dir = FHSRoot p.cmd.Dir = "/"
p.cmd.SysProcAttr = &SysProcAttr{ p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession, Setsid: !p.RetainSession,
Pdeathsig: SIGKILL, Pdeathsig: SIGKILL,
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS | Cloneflags: cloneFlags | CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS,
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
AmbientCaps: []uintptr{ // remain privileged for setup
// general container setup AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP},
CAP_SYS_ADMIN,
// drop capabilities
CAP_SETPCAP,
// overlay access to upperdir and workdir
CAP_DAC_OVERRIDE,
},
UseCgroupFD: p.Cgroup != nil, UseCgroupFD: p.Cgroup != nil,
} }
if p.cmd.SysProcAttr.UseCgroupFD { if p.cmd.SysProcAttr.UseCgroupFD {
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
} }
if !p.HostNet {
p.cmd.SysProcAttr.Cloneflags |= CLONE_NEWNET
}
// place setup pipe before user supplied extra files, this is later restored by init // place setup pipe before user supplied extra files, this is later restored by init
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil { if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
@ -179,8 +164,6 @@ func (p *Container) Start() error {
return nil return nil
} }
// Serve serves [Container.Params] to the container init.
// Serve must only be called once.
func (p *Container) Serve() error { func (p *Container) Serve() error {
if p.setup == nil { if p.setup == nil {
panic("invalid serve") panic("invalid serve")
@ -189,16 +172,33 @@ func (p *Container) Serve() error {
setup := p.setup setup := p.setup
p.setup = nil p.setup = nil
if p.Path == nil { if p.Path != "" && !path.IsAbs(p.Path) {
p.cancel() p.cancel()
return msg.WrapErr(EINVAL, "invalid executable pathname") return msg.WrapErr(EINVAL,
fmt.Sprintf("invalid executable path %q", p.Path))
} }
// do not transmit nil if p.Path == "" {
if p.Dir == nil { if p.name == "" {
p.Dir = AbsFHSRoot p.Path = os.Getenv("SHELL")
if !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(EBADE,
"no command specified and $SHELL is invalid")
}
p.name = path.Base(p.Path)
} else if path.IsAbs(p.name) {
p.Path = p.name
} else if v, err := exec.LookPath(p.name); err != nil {
p.cancel()
return msg.WrapErr(err, err.Error())
} else {
p.Path = v
}
} }
if p.SeccompRules == nil { if p.SeccompRules == nil {
// do not transmit nil
p.SeccompRules = make([]seccomp.NativeRule, 0) p.SeccompRules = make([]seccomp.NativeRule, 0)
} }
@ -217,7 +217,6 @@ func (p *Container) Serve() error {
return err return err
} }
// Wait waits for the container init process to exit.
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() } func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
func (p *Container) String() string { func (p *Container) String() string {
@ -225,23 +224,8 @@ func (p *Container) String() string {
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets)) p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
} }
// ProcessState returns the address to os.ProcessState held by the underlying [exec.Cmd]. func New(ctx context.Context, name string, args ...string) *Container {
func (p *Container) ProcessState() *os.ProcessState { return &Container{name: name, ctx: ctx,
if p.cmd == nil { Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
return nil
} }
return p.cmd.ProcessState
}
// New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context) *Container {
return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
}
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container {
z := New(ctx)
z.Path = pathname
z.Args = append([]string{name}, args...)
return z
} }

View File

@ -4,326 +4,172 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/gob" "encoding/gob"
"errors"
"fmt"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"strconv"
"strings" "strings"
"syscall" "syscall"
"testing" "testing"
"time"
"hakurei.app/command"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
) )
const ( const (
ignore = "\x00" ignore = "\x00"
ignoreV = -1 ignoreV = -1
pathPrefix = "/etc/hakurei/"
pathWantMnt = pathPrefix + "want-mnt"
pathReadonly = pathPrefix + "readonly"
) )
type testVal any
func emptyOps(t *testing.T) (*container.Ops, context.Context) { return new(container.Ops), t.Context() }
func earlyOps(ops *container.Ops) func(t *testing.T) (*container.Ops, context.Context) {
return func(t *testing.T) (*container.Ops, context.Context) { return ops, t.Context() }
}
func emptyMnt(*testing.T, context.Context) []*vfs.MountInfoEntry { return nil }
func earlyMnt(mnt ...*vfs.MountInfoEntry) func(*testing.T, context.Context) []*vfs.MountInfoEntry {
return func(*testing.T, context.Context) []*vfs.MountInfoEntry { return mnt }
}
var containerTestCases = []struct {
name string
filter bool
session bool
net bool
ro bool
ops func(t *testing.T) (*container.Ops, context.Context)
mnt func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry
uid int
gid int
rules []seccomp.NativeRule
flags seccomp.ExportFlag
presets seccomp.FilterPreset
}{
{"minimal", true, false, false, true,
emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetStrict},
{"allow", true, true, true, false,
emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
{"no filter", false, true, true, true,
emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true, false,
emptyOps, emptyMnt,
1, 31, []seccomp.NativeRule{{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}}, 0, seccomp.PresetExt},
{"tmpfs", true, false, false, true,
earlyOps(new(container.Ops).
Tmpfs(hst.AbsTmp, 0, 0755),
),
earlyMnt(
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
),
9, 9, nil, 0, seccomp.PresetStrict},
{"dev", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops).
Dev(container.MustAbs("/dev"), true),
),
earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
),
1971, 100, nil, 0, seccomp.PresetStrict},
{"dev no mqueue", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops).
Dev(container.MustAbs("/dev"), false),
),
earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
),
1971, 100, nil, 0, seccomp.PresetStrict},
{"overlay", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir())
lower0, lower1, upper, work :=
tempDir.Append("lower0"),
tempDir.Append("lower1"),
tempDir.Append("upper"),
tempDir.Append("work")
for _, a := range []*container.Absolute{lower0, lower1, upper, work} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
Overlay(hst.AbsTmp, upper, work, lower0, lower1),
context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1),
testVal("lower0"), lower0),
testVal("work"), work),
testVal("upper"), upper)
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay",
"rw,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",upperdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+
",workdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+
",redirect_dir=nofollow,uuid=on,userxattr"),
}
},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
{"overlay ephemeral", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir())
lower0, lower1 :=
tempDir.Append("lower0"),
tempDir.Append("lower1")
for _, a := range []*container.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
OverlayEphemeral(hst.AbsTmp, lower0, lower1),
t.Context()
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
// contains random suffix
ent("/", hst.Tmp, "rw", "overlay", "overlay", ignore),
}
},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
{"overlay readonly", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir())
lower0, lower1 :=
tempDir.Append("lower0"),
tempDir.Append("lower1")
for _, a := range []*container.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
OverlayReadonly(hst.AbsTmp, lower0, lower1),
context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1),
testVal("lower0"), lower0)
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay",
"ro,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",redirect_dir=nofollow,userxattr"),
}
},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
}
func TestContainer(t *testing.T) { func TestContainer(t *testing.T) {
replaceOutput(t) {
oldVerbose := hlog.Load()
oldOutput := container.GetOutput()
internal.InstallOutput(true)
t.Cleanup(func() { hlog.Store(oldVerbose) })
t.Cleanup(func() { container.SetOutput(oldOutput) })
}
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) { testCases := []struct {
wantErr := context.Canceled name string
wantExitCode := 0 filter bool
if err := c.Wait(); !errors.Is(err, wantErr) { session bool
container.GetOutput().PrintBaseErr(err, "wait:") net bool
t.Errorf("Wait: error = %v, want %v", err, wantErr) ops *container.Ops
} mnt []*vfs.MountInfoEntry
if ps := c.ProcessState(); ps == nil { host string
t.Errorf("ProcessState unexpectedly returned nil") rules []seccomp.NativeRule
} else if code := ps.ExitCode(); code != wantExitCode { flags seccomp.ExportFlag
t.Errorf("ExitCode: %d, want %d", code, wantExitCode) presets seccomp.FilterPreset
} }{
})) {"minimal", true, false, false,
new(container.Ops), nil, "test-minimal",
nil, 0, seccomp.PresetStrict},
{"allow", true, true, true,
new(container.Ops), nil, "test-minimal",
nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
{"no filter", false, true, true,
new(container.Ops), nil, "test-no-filter",
nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true,
new(container.Ops), nil, "test-no-filter",
[]seccomp.NativeRule{
{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil},
}, 0, seccomp.PresetExt},
{"tmpfs", true, false, false,
new(container.Ops).
Tmpfs(hst.Tmp, 0, 0755),
[]*vfs.MountInfoEntry{
e("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
}, "test-tmpfs",
nil, 0, seccomp.PresetStrict},
{"dev", true, true /* go test output is not a tty */, false,
new(container.Ops).
Dev("/dev").
Mqueue("/dev/mqueue"),
[]*vfs.MountInfoEntry{
e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
}, "",
nil, 0, seccomp.PresetStrict},
}
t.Run("forward", testContainerCancel(func(c *container.Container) { for _, tc := range testCases {
c.ForwardCancel = true
}, func(t *testing.T, c *container.Container) {
var exitError *exec.ExitError
if err := c.Wait(); !errors.As(err, &exitError) {
container.GetOutput().PrintBaseErr(err, "wait:")
t.Errorf("Wait: error = %v", err)
}
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
}
}))
for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
wantOps, wantOpsCtx := tc.ops(t) ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel() defer cancel()
var libPaths []*container.Absolute c := container.New(ctx, "/usr/bin/sandbox.test", "-test.v",
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i)) "-test.run=TestHelperCheckContainer", "--", "check", tc.host)
c.Uid = tc.uid c.Uid = 1000
c.Gid = tc.gid c.Gid = 100
c.Hostname = hostnameFromTestCase(tc.name) c.Hostname = tc.host
output := new(bytes.Buffer) c.CommandContext = commandContext
if !testing.Verbose() { c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.Stdout, c.Stderr = output, output c.Ops = tc.ops
} else {
c.Stdout, c.Stderr = os.Stdout, os.Stderr
}
c.WaitDelay = helperDefaultTimeout
*c.Ops = append(*c.Ops, *wantOps...)
c.SeccompRules = tc.rules c.SeccompRules = tc.rules
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
c.SeccompPresets = tc.presets c.SeccompPresets = tc.presets
c.SeccompDisable = !tc.filter c.SeccompDisable = !tc.filter
c.RetainSession = tc.session c.RetainSession = tc.session
c.HostNet = tc.net c.HostNet = tc.net
if c.Args[5] == "" {
if name, err := os.Hostname(); err != nil {
t.Fatalf("cannot get hostname: %v", err)
} else {
c.Args[5] = name
}
}
c. c.
Readonly(container.MustAbs(pathReadonly), 0755). Tmpfs("/tmp", 0, 0755).
Tmpfs(container.MustAbs("/tmp"), 0, 0755). Bind(os.Args[0], os.Args[0], 0).
Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname)) Mkdir("/usr/bin", 0755).
// needs /proc to check mountinfo Link(os.Args[0], "/usr/bin/sandbox.test").
c.Proc(container.MustAbs("/proc")) Place("/etc/hostname", []byte(c.Args[5]))
// in case test has cgo enabled
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism var libPaths []string
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) if entries, err := ldd.ExecFilter(ctx,
mnt = append(mnt, commandContext,
ent("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore), func(v []byte) []byte {
// Bind(os.Args[0], helperInnerPath, 0) return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1]
ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore), }, os.Args[0]); err != nil {
) log.Fatalf("ldd: %v", err)
for _, a := range libPaths { } else {
// Bind(name, name, 0) libPaths = ldd.Path(entries)
mnt = append(mnt, ent(ignore, a.String(), "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
} }
mnt = append(mnt, wantMnt...) for _, name := range libPaths {
c.Bind(name, name, 0)
}
// needs /proc to check mountinfo
c.Proc("/proc")
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
mnt = append(mnt, tc.mnt...)
mnt = append(mnt, mnt = append(mnt,
// Readonly(pathReadonly, 0755) e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
ent("/", pathReadonly, "ro,nosuid,nodev", "tmpfs", "readonly", ignore), e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
// Tmpfs("/tmp", 0, 0755) e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
// Place("/etc/hostname", []byte(hostname))
ent(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
// Proc("/proc")
ent("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"),
// Place(pathWantMnt, want.Bytes())
ent(ignore, pathWantMnt, "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
) )
for _, name := range libPaths {
mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
}
mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"))
want := new(bytes.Buffer) want := new(bytes.Buffer)
if err := gob.NewEncoder(want).Encode(mnt); err != nil { if err := gob.NewEncoder(want).Encode(mnt); err != nil {
_, _ = output.WriteTo(os.Stdout)
t.Fatalf("cannot serialise expected mount points: %v", err) t.Fatalf("cannot serialise expected mount points: %v", err)
} }
c.Place(container.MustAbs(pathWantMnt), want.Bytes()) c.Stdin = want
if tc.ro {
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
}
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
_, _ = output.WriteTo(os.Stdout) hlog.PrintBaseError(err, "start:")
container.GetOutput().PrintBaseErr(err, "start:")
t.Fatalf("cannot start container: %v", err) t.Fatalf("cannot start container: %v", err)
} else if err = c.Serve(); err != nil { } else if err = c.Serve(); err != nil {
_, _ = output.WriteTo(os.Stdout) hlog.PrintBaseError(err, "serve:")
container.GetOutput().PrintBaseErr(err, "serve:")
t.Errorf("cannot serve setup params: %v", err) t.Errorf("cannot serve setup params: %v", err)
} }
if err := c.Wait(); err != nil { if err := c.Wait(); err != nil {
_, _ = output.WriteTo(os.Stdout) hlog.PrintBaseError(err, "wait:")
container.GetOutput().PrintBaseErr(err, "wait:")
t.Fatalf("wait: %v", err) t.Fatalf("wait: %v", err)
} }
}) })
} }
} }
func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry { func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
return &vfs.MountInfoEntry{ return &vfs.MountInfoEntry{
ID: ignoreV, ID: ignoreV,
Parent: ignoreV, Parent: ignoreV,
@ -338,52 +184,8 @@ func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInf
} }
} }
func hostnameFromTestCase(name string) string {
return "test-" + strings.Join(strings.Fields(name), "-")
}
func testContainerCancel(
containerExtra func(c *container.Container),
waitCheck func(t *testing.T, c *container.Container),
) func(t *testing.T) {
return func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block")
c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.WaitDelay = helperDefaultTimeout
if containerExtra != nil {
containerExtra(c)
}
ready := make(chan struct{})
if r, w, err := os.Pipe(); err != nil {
t.Fatalf("cannot pipe: %v", err)
} else {
c.ExtraFiles = append(c.ExtraFiles, w)
go func() {
defer close(ready)
if _, err = r.Read(make([]byte, 1)); err != nil {
panic(err.Error())
}
}()
}
if err := c.Start(); err != nil {
container.GetOutput().PrintBaseErr(err, "start:")
t.Fatalf("cannot start container: %v", err)
} else if err = c.Serve(); err != nil {
container.GetOutput().PrintBaseErr(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
}
<-ready
cancel()
waitCheck(t, c)
}
}
func TestContainerString(t *testing.T) { func TestContainerString(t *testing.T) {
c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env") c := container.New(t.Context(), "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,
@ -395,118 +197,85 @@ func TestContainerString(t *testing.T) {
} }
} }
const ( func TestHelperInit(t *testing.T) {
blockExitCodeInterrupt = 2 if len(os.Args) != 5 || os.Args[4] != "init" {
) return
}
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, internal.InstallOutput)
}
func init() { func TestHelperCheckContainer(t *testing.T) {
helperCommands = append(helperCommands, func(c command.Command) { if len(os.Args) != 6 || os.Args[4] != "check" {
c.Command("block", command.UsageInternal, func(args []string) error { return
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil { }
return fmt.Errorf("write to sync pipe: %v", err)
}
{
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
}
select {}
})
c.Command("container", command.UsageInternal, func(args []string) error { t.Run("user", func(t *testing.T) {
if len(args) != 1 { if uid := syscall.Getuid(); uid != 1000 {
return syscall.EINVAL t.Errorf("Getuid: %d, want 1000", uid)
}
if gid := syscall.Getgid(); gid != 100 {
t.Errorf("Getgid: %d, want 100", gid)
}
})
t.Run("hostname", func(t *testing.T) {
if name, err := os.Hostname(); err != nil {
t.Fatalf("cannot get hostname: %v", err)
} else if name != os.Args[5] {
t.Errorf("Hostname: %q, want %q", name, os.Args[5])
}
if p, err := os.ReadFile("/etc/hostname"); err != nil {
t.Fatalf("%v", err)
} else if string(p) != os.Args[5] {
t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
}
})
t.Run("mount", func(t *testing.T) {
var mnt []*vfs.MountInfoEntry
if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
t.Fatalf("cannot receive expected mount points: %v", err)
}
var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
t.Fatalf("cannot open mountinfo: %v", err)
} else {
d = vfs.NewMountInfoDecoder(f)
}
i := 0
for cur := range d.Entries() {
if i == len(mnt) {
t.Errorf("got more than %d entries", len(mnt))
break
} }
tc := containerTestCases[0]
if i, err := strconv.Atoi(args[0]); err != nil { // ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
return fmt.Errorf("cannot parse test case index: %v", err) cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") {
t.Errorf("[FAIL] %s", cur)
} else { } else {
tc = containerTestCases[i] t.Logf("[ OK ] %s", cur)
} }
if uid := syscall.Getuid(); uid != tc.uid { i++
return fmt.Errorf("uid: %d, want %d", uid, tc.uid) }
} if err := d.Err(); err != nil {
if gid := syscall.Getgid(); gid != tc.gid { t.Errorf("cannot parse mountinfo: %v", err)
return fmt.Errorf("gid: %d, want %d", gid, tc.gid) }
}
wantHost := hostnameFromTestCase(tc.name) if i != len(mnt) {
if host, err := os.Hostname(); err != nil { t.Errorf("got %d entries, want %d", i, len(mnt))
return fmt.Errorf("cannot get hostname: %v", err) }
} else if host != wantHost {
return fmt.Errorf("hostname: %q, want %q", host, wantHost)
}
if p, err := os.ReadFile("/etc/hostname"); err != nil {
return fmt.Errorf("cannot read /etc/hostname: %v", err)
} else if string(p) != wantHost {
return fmt.Errorf("/etc/hostname: %q, want %q", string(p), wantHost)
}
if _, err := os.Create(pathReadonly + "/nonexistent"); !errors.Is(err, syscall.EROFS) {
return err
}
{
var fail bool
var mnt []*vfs.MountInfoEntry
if f, err := os.Open(pathWantMnt); err != nil {
return fmt.Errorf("cannot open expected mount points: %v", err)
} else if err = gob.NewDecoder(f).Decode(&mnt); err != nil {
return fmt.Errorf("cannot parse expected mount points: %v", err)
} else if err = f.Close(); err != nil {
return fmt.Errorf("cannot close expected mount points: %v", err)
}
if tc.ro && len(mnt) > 0 {
// Remount("/", syscall.MS_RDONLY)
mnt[0].VfsOptstr = "ro,nosuid,nodev"
}
var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
return fmt.Errorf("cannot open mountinfo: %v", err)
} else {
d = vfs.NewMountInfoDecoder(f)
}
i := 0
for cur := range d.Entries() {
if i == len(mnt) {
return fmt.Errorf("got more than %d entries", len(mnt))
}
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") {
fail = true
log.Printf("[FAIL] %s", cur)
} else {
log.Printf("[ OK ] %s", cur)
}
i++
}
if err := d.Err(); err != nil {
return fmt.Errorf("cannot parse mountinfo: %v", err)
}
if i != len(mnt) {
return fmt.Errorf("got %d entries, want %d", i, len(mnt))
}
if fail {
return errors.New("one or more mountinfo entries do not match")
}
}
return nil
})
}) })
} }
func commandContext(ctx context.Context) *exec.Cmd {
return exec.CommandContext(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperInit", "--", "init")
}

View File

@ -18,6 +18,9 @@ import (
) )
const ( const (
// time to wait for linger processes after death of initial process
residualProcessTimeout = 5 * time.Second
/* intermediate tmpfs mount point /* intermediate tmpfs mount point
this path might seem like a weird choice, however there are many good reasons to use it: this path might seem like a weird choice, however there are many good reasons to use it:
@ -32,7 +35,7 @@ const (
it should be noted that none of this should become relevant at any point since the resulting it should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous */ intermediate root tmpfs should be effectively anonymous */
intermediateHostPath = FHSProc + "self/fd" intermediateHostPath = "/proc/self/fd"
// setup params file descriptor // setup params file descriptor
setupEnv = "HAKUREI_SETUP" setupEnv = "HAKUREI_SETUP"
@ -63,7 +66,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
offsetSetup int offsetSetup int
) )
if f, err := Receive(setupEnv, &params, &setupFile); err != nil { if f, err := Receive(setupEnv, &params, &setupFile); err != nil {
if errors.Is(err, EBADF) { if errors.Is(err, ErrInvalid) {
log.Fatal("invalid setup descriptor") log.Fatal("invalid setup descriptor")
} }
if errors.Is(err, ErrNotSet) { if errors.Is(err, ErrNotSet) {
@ -89,17 +92,17 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if err := SetDumpable(SUID_DUMP_USER); err != nil { if err := SetDumpable(SUID_DUMP_USER); err != nil {
log.Fatalf("cannot set SUID_DUMP_USER: %s", err) log.Fatalf("cannot set SUID_DUMP_USER: %s", err)
} }
if err := os.WriteFile(FHSProc+"self/uid_map", if err := os.WriteFile("/proc/self/uid_map",
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...), append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
0); err != nil { 0); err != nil {
log.Fatalf("%v", err) log.Fatalf("%v", err)
} }
if err := os.WriteFile(FHSProc+"self/setgroups", if err := os.WriteFile("/proc/self/setgroups",
[]byte("deny\n"), []byte("deny\n"),
0); err != nil && !os.IsNotExist(err) { 0); err != nil && !os.IsNotExist(err) {
log.Fatalf("%v", err) log.Fatalf("%v", err)
} }
if err := os.WriteFile(FHSProc+"self/gid_map", if err := os.WriteFile("/proc/self/gid_map",
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...), append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
0); err != nil { 0); err != nil {
log.Fatalf("%v", err) log.Fatalf("%v", err)
@ -118,22 +121,16 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
// cache sysctl before pivot_root // cache sysctl before pivot_root
LastCap() LastCap()
if err := Mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil { if err := Mount("", "/", "", MS_SILENT|MS_SLAVE|MS_REC, ""); err != nil {
log.Fatalf("cannot make / rslave: %v", err) log.Fatalf("cannot make / rslave: %v", err)
} }
state := &setupState{Params: &params.Params}
/* early is called right before pivot_root into intermediate root;
this step is mostly for gathering information that would otherwise be difficult to obtain
via library functions after pivot_root, and implementations are expected to avoid changing
the state of the mount namespace */
for i, op := range *params.Ops { for i, op := range *params.Ops {
if op == nil { if op == nil {
log.Fatalf("invalid op %d", i) log.Fatalf("invalid op %d", i)
} }
if err := op.early(state); err != nil { if err := op.early(&params.Params); err != nil {
msg.PrintBaseErr(err, msg.PrintBaseErr(err,
fmt.Sprintf("cannot prepare op %d:", i)) fmt.Sprintf("cannot prepare op %d:", i))
msg.BeforeExit() msg.BeforeExit()
@ -141,7 +138,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
} }
if err := Mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil { if err := Mount("rootfs", intermediateHostPath, "tmpfs", MS_NODEV|MS_NOSUID, ""); err != nil {
log.Fatalf("cannot mount intermediate root: %v", err) log.Fatalf("cannot mount intermediate root: %v", err)
} }
if err := os.Chdir(intermediateHostPath); err != nil { if err := os.Chdir(intermediateHostPath); err != nil {
@ -151,7 +148,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if err := os.Mkdir(sysrootDir, 0755); err != nil { if err := os.Mkdir(sysrootDir, 0755); err != nil {
log.Fatalf("%v", err) log.Fatalf("%v", err)
} }
if err := Mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil { if err := Mount(sysrootDir, sysrootDir, "", MS_SILENT|MS_MGC_VAL|MS_BIND|MS_REC, ""); err != nil {
log.Fatalf("cannot bind sysroot: %v", err) log.Fatalf("cannot bind sysroot: %v", err)
} }
@ -162,18 +159,14 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if err := PivotRoot(intermediateHostPath, hostDir); err != nil { if err := PivotRoot(intermediateHostPath, hostDir); err != nil {
log.Fatalf("cannot pivot into intermediate root: %v", err) log.Fatalf("cannot pivot into intermediate root: %v", err)
} }
if err := os.Chdir(FHSRoot); err != nil { if err := os.Chdir("/"); err != nil {
log.Fatalf("%v", err) log.Fatalf("%v", err)
} }
/* apply is called right after pivot_root and entering the new root;
this step sets up the container filesystem, and implementations are expected to keep the host root
and sysroot mount points intact but otherwise can do whatever they need to;
chdir is allowed but discouraged */
for i, op := range *params.Ops { for i, op := range *params.Ops {
// ops already checked during early setup // ops already checked during early setup
msg.Verbosef("%s %s", op.prefix(), op) msg.Verbosef("%s %s", op.prefix(), op)
if err := op.apply(state); err != nil { if err := op.apply(&params.Params); err != nil {
msg.PrintBaseErr(err, msg.PrintBaseErr(err,
fmt.Sprintf("cannot apply op %d:", i)) fmt.Sprintf("cannot apply op %d:", i))
msg.BeforeExit() msg.BeforeExit()
@ -182,7 +175,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
// setup requiring host root complete at this point // setup requiring host root complete at this point
if err := Mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil { if err := Mount(hostDir, hostDir, "", MS_SILENT|MS_REC|MS_PRIVATE, ""); err != nil {
log.Fatalf("cannot make host root rprivate: %v", err) log.Fatalf("cannot make host root rprivate: %v", err)
} }
if err := Unmount(hostDir, MNT_DETACH); err != nil { if err := Unmount(hostDir, MNT_DETACH); err != nil {
@ -192,7 +185,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
{ {
var fd int var fd int
if err := IgnoringEINTR(func() (err error) { if err := IgnoringEINTR(func() (err error) {
fd, err = Open(FHSRoot, O_DIRECTORY|O_RDONLY, 0) fd, err = Open("/", O_DIRECTORY|O_RDONLY, 0)
return return
}); err != nil { }); err != nil {
log.Fatalf("cannot open intermediate root: %v", err) log.Fatalf("cannot open intermediate root: %v", err)
@ -210,7 +203,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if err := Unmount(".", MNT_DETACH); err != nil { if err := Unmount(".", MNT_DETACH); err != nil {
log.Fatalf("cannot unmount intemediate root: %v", err) log.Fatalf("cannot unmount intemediate root: %v", err)
} }
if err := os.Chdir(FHSRoot); err != nil { if err := os.Chdir("/"); err != nil {
log.Fatalf("%v", err) log.Fatalf("%v", err)
} }
@ -277,21 +270,20 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
Umask(oldmask) Umask(oldmask)
cmd := exec.Command(params.Path.String()) cmd := exec.Command(params.Path)
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.String() cmd.Dir = params.Dir
msg.Verbosef("starting initial program %s", params.Path)
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
log.Fatalf("%v", err) log.Fatalf("%v", err)
} }
msg.Suspend() msg.Suspend()
if err := closeSetup(); err != nil { if err := closeSetup(); err != nil {
log.Printf("cannot close setup pipe: %v", err) log.Println("cannot close setup pipe:", err)
// not fatal // not fatal
} }
@ -325,7 +317,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
} }
if !errors.Is(err, ECHILD) { if !errors.Is(err, ECHILD) {
log.Printf("unexpected wait4 response: %v", err) log.Println("unexpected wait4 response:", err)
} }
close(done) close(done)
@ -333,7 +325,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
// handle signals to dump withheld messages // handle signals to dump withheld messages
sig := make(chan os.Signal, 2) sig := make(chan os.Signal, 2)
signal.Notify(sig, os.Interrupt, CancelSignal) signal.Notify(sig, SIGINT, SIGTERM)
// closed after residualProcessTimeout has elapsed after initial process death // closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{}) timeout := make(chan struct{})
@ -343,16 +335,9 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
select { select {
case s := <-sig: case s := <-sig:
if msg.Resume() { if msg.Resume() {
msg.Verbosef("%s after process start", s.String()) msg.Verbosef("terminating on %s after process start", s.String())
} else { } else {
msg.Verbosef("got %s", s.String()) msg.Verbosef("terminating on %s", s.String())
}
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
msg.Verbose("forwarding context cancellation")
if err := cmd.Process.Signal(os.Interrupt); err != nil {
log.Printf("cannot forward cancellation: %v", err)
}
continue
} }
os.Exit(0) os.Exit(0)
case w := <-info: case w := <-info:
@ -372,7 +357,10 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
msg.Verbosef("initial process exited with status %#x", w.wstatus) msg.Verbosef("initial process exited with status %#x", w.wstatus)
} }
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }() go func() {
time.Sleep(residualProcessTimeout)
close(timeout)
}()
} }
case <-done: case <-done:
msg.BeforeExit() msg.BeforeExit()
@ -385,11 +373,9 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
} }
const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init". // TryArgv0 calls [Init] if the last element of argv0 is "init".
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) { func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName { if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
msg = v msg = v
Init(prepare, setVerbose) Init(prepare, setVerbose)
msg.BeforeExit() msg.BeforeExit()

View File

@ -1,73 +0,0 @@
package container_test
import (
"context"
"log"
"os"
"testing"
"time"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
)
const (
envDoCheck = "HAKUREI_TEST_DO_CHECK"
helperDefaultTimeout = 5 * time.Second
helperInnerPath = "/usr/bin/helper"
)
var (
absHelperInnerPath = container.MustAbs(helperInnerPath)
)
var helperCommands []func(c command.Command)
func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
if os.Getenv(envDoCheck) == "1" {
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
log.SetFlags(0)
log.SetPrefix("helper: ")
return nil
})
for _, f := range helperCommands {
f(c)
}
c.MustParse(os.Args[1:], func(err error) {
if err != nil {
log.Fatal(err.Error())
}
})
return
}
os.Exit(m.Run())
}
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) {
c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0)
// in case test has cgo enabled
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
*libPaths = ldd.Path(entries)
}
for _, name := range *libPaths {
c.Bind(name, name, 0)
}
return
}
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...)
}

View File

@ -5,96 +5,11 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
. "syscall" . "syscall"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
) )
/*
Holding CAP_SYS_ADMIN within the user namespace that owns a process's mount namespace
allows that process to create bind mounts and mount the following types of filesystems:
- /proc (since Linux 3.8)
- /sys (since Linux 3.8)
- devpts (since Linux 3.9)
- tmpfs(5) (since Linux 3.9)
- ramfs (since Linux 3.9)
- mqueue (since Linux 3.9)
- bpf (since Linux 4.4)
- overlayfs (since Linux 5.11)
*/
const (
// zeroString is a zero value string, it represents NULL when passed to mount.
zeroString = ""
// SourceNone is used when the source value is ignored,
// such as when remounting.
SourceNone = "none"
// SourceProc is used when mounting proc.
// Note that any source value is allowed when fstype is [FstypeProc].
SourceProc = "proc"
// SourceDevpts is used when mounting devpts.
// Note that any source value is allowed when fstype is [FstypeDevpts].
SourceDevpts = "devpts"
// SourceMqueue is used when mounting mqueue.
// Note that any source value is allowed when fstype is [FstypeMqueue].
SourceMqueue = "mqueue"
// SourceOverlay is used when mounting overlay.
// Note that any source value is allowed when fstype is [FstypeOverlay].
SourceOverlay = "overlay"
// SourceTmpfsRootfs is used when mounting the tmpfs instance backing the intermediate root.
SourceTmpfsRootfs = "rootfs"
// SourceTmpfsDevtmpfs is used when mounting tmpfs representing a subset of host devtmpfs.
SourceTmpfsDevtmpfs = "devtmpfs"
// SourceTmpfsEphemeral is used when mounting a writable instance of tmpfs.
SourceTmpfsEphemeral = "ephemeral"
// SourceTmpfsReadonly is used when mounting a readonly instance of tmpfs.
SourceTmpfsReadonly = "readonly"
// FstypeNULL is used when the fstype value is ignored,
// such as when bind mounting or remounting.
FstypeNULL = zeroString
// FstypeProc represents the proc pseudo-filesystem.
// A fully visible instance of proc must be available in the mount namespace for proc to be mounted.
// This filesystem type is usually mounted on [FHSProc].
FstypeProc = "proc"
// FstypeDevpts represents the devpts pseudo-filesystem.
// This type of filesystem is usually mounted on /dev/pts.
FstypeDevpts = "devpts"
// FstypeTmpfs represents the tmpfs filesystem.
// This filesystem type can be mounted anywhere in the container filesystem.
FstypeTmpfs = "tmpfs"
// FstypeMqueue represents the mqueue pseudo-filesystem.
// This filesystem type is usually mounted on /dev/mqueue.
FstypeMqueue = "mqueue"
// FstypeOverlay represents the overlay pseudo-filesystem.
// This filesystem type can be mounted anywhere in the container filesystem.
FstypeOverlay = "overlay"
// OptionOverlayLowerdir represents the lowerdir option of the overlay pseudo-filesystem.
// Any filesystem, does not need to be on a writable filesystem.
OptionOverlayLowerdir = "lowerdir"
// OptionOverlayUpperdir represents the upperdir option of the overlay pseudo-filesystem.
// The upperdir is normally on a writable filesystem.
OptionOverlayUpperdir = "upperdir"
// OptionOverlayWorkdir represents the workdir option of the overlay pseudo-filesystem.
// The workdir needs to be an empty directory on the same filesystem as upperdir.
OptionOverlayWorkdir = "workdir"
// OptionOverlayUserxattr represents the userxattr option of the overlay pseudo-filesystem.
// Use the "user.overlay." xattr namespace instead of "trusted.overlay.".
OptionOverlayUserxattr = "userxattr"
// SpecialOverlayEscape is the escape string for overlay mount options.
SpecialOverlayEscape = `\`
// SpecialOverlayOption is the separator string between overlay mount options.
SpecialOverlayOption = ","
// SpecialOverlayPath is the separator string between overlay paths.
SpecialOverlayPath = ":"
)
// bindMount mounts source on target and recursively applies flags if MS_REC is set.
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error { func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
if eq { if eq {
msg.Verbosef("resolved %q flags %#x", target, flags) msg.Verbosef("resolved %q flags %#x", target, flags)
@ -102,16 +17,11 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
msg.Verbosef("resolved %q on %q flags %#x", source, target, flags) msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
} }
if err := Mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil { if err := Mount(source, target, "", MS_SILENT|MS_BIND|flags&MS_REC, ""); err != nil {
return wrapErrSuffix(err, return wrapErrSuffix(err,
fmt.Sprintf("cannot mount %q on %q:", source, target)) fmt.Sprintf("cannot mount %q on %q:", source, target))
} }
return p.remount(target, flags)
}
// remount applies flags on target, recursively if MS_REC is set.
func (p *procPaths) remount(target string, flags uintptr) error {
var targetFinal string var targetFinal string
if v, err := filepath.EvalSymlinks(target); err != nil { if v, err := filepath.EvalSymlinks(target); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
@ -173,7 +83,6 @@ func (p *procPaths) remount(target string, flags uintptr) error {
}) })
} }
// remountWithFlags remounts mount point described by [vfs.MountInfoNode].
func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error { func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
kf, unmatched := n.Flags() kf, unmatched := n.Flags()
if len(unmatched) != 0 { if len(unmatched) != 0 {
@ -182,15 +91,14 @@ func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
if kf&mf != mf { if kf&mf != mf {
return wrapErrSuffix( return wrapErrSuffix(
Mount(SourceNone, n.Clean, FstypeNULL, MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, zeroString), Mount("none", n.Clean, "", MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, ""),
fmt.Sprintf("cannot remount %q:", n.Clean)) fmt.Sprintf("cannot remount %q:", n.Clean))
} }
return nil return nil
} }
// mountTmpfs mounts tmpfs on target; func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
// callers who wish to mount to sysroot must pass the return value of toSysroot. target := toSysroot(name)
func mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
if err := os.MkdirAll(target, parentPerm(perm)); err != nil { if err := os.MkdirAll(target, parentPerm(perm)); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} }
@ -199,8 +107,8 @@ func mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode
opt += fmt.Sprintf(",size=%d", size) opt += fmt.Sprintf(",size=%d", size)
} }
return wrapErrSuffix( return wrapErrSuffix(
Mount(fsname, target, FstypeTmpfs, flags, opt), Mount(fsname, target, "tmpfs", MS_NOSUID|MS_NODEV, opt),
fmt.Sprintf("cannot mount tmpfs on %q:", target)) fmt.Sprintf("cannot mount tmpfs on %q:", name))
} }
func parentPerm(perm os.FileMode) os.FileMode { func parentPerm(perm os.FileMode) os.FileMode {
@ -213,20 +121,3 @@ func parentPerm(perm os.FileMode) os.FileMode {
} }
return os.FileMode(pperm) return os.FileMode(pperm)
} }
// EscapeOverlayDataSegment escapes a string for formatting into the data argument of an overlay mount call.
func EscapeOverlayDataSegment(s string) string {
if s == zeroString {
return zeroString
}
if f := strings.SplitN(s, "\x00", 2); len(f) > 0 {
s = f[0]
}
return strings.NewReplacer(
SpecialOverlayEscape, SpecialOverlayEscape+SpecialOverlayEscape,
SpecialOverlayOption, SpecialOverlayEscape+SpecialOverlayOption,
SpecialOverlayPath, SpecialOverlayEscape+SpecialOverlayPath,
).Replace(s)
}

View File

@ -1,49 +0,0 @@
package container
import (
"os"
"testing"
)
func TestParentPerm(t *testing.T) {
testCases := []struct {
perm os.FileMode
want os.FileMode
}{
{0755, 0755},
{0750, 0750},
{0705, 0705},
{0700, 0700},
{050, 0750},
{05, 0705},
{0, 0700},
}
for _, tc := range testCases {
t.Run(tc.perm.String(), func(t *testing.T) {
if got := parentPerm(tc.perm); got != tc.want {
t.Errorf("parentPerm: %#o, want %#o", got, tc.want)
}
})
}
}
func TestEscapeOverlayDataSegment(t *testing.T) {
testCases := []struct {
name string
s string
want string
}{
{"zero", zeroString, zeroString},
{"multi", `\\\:,:,\\\`, `\\\\\\\:\,\:\,\\\\\\`},
{"bwrap", `/path :,\`, `/path \:\,\\`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := EscapeOverlayDataSegment(tc.s); got != tc.want {
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
}
})
}
}

View File

@ -1,139 +0,0 @@
package container_test
import (
"log"
"strings"
"sync/atomic"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/internal/hlog"
)
func TestDefaultMsg(t *testing.T) {
{
w := log.Writer()
f := log.Flags()
t.Cleanup(func() { log.SetOutput(w); log.SetFlags(f) })
}
msg := new(container.DefaultMsg)
t.Run("is verbose", func(t *testing.T) {
if !msg.IsVerbose() {
t.Error("IsVerbose unexpected outcome")
}
})
t.Run("verbose", func(t *testing.T) {
log.SetOutput(panicWriter{})
msg.Suspend()
msg.Verbose()
msg.Verbosef("\x00")
msg.Resume()
buf := new(strings.Builder)
log.SetOutput(buf)
log.SetFlags(0)
msg.Verbose()
msg.Verbosef("\x00")
want := "\n\x00\n"
if buf.String() != want {
t.Errorf("Verbose: %q, want %q", buf.String(), want)
}
})
t.Run("wrapErr", func(t *testing.T) {
buf := new(strings.Builder)
log.SetOutput(buf)
log.SetFlags(0)
if err := msg.WrapErr(syscall.EBADE, "\x00", "\x00"); err != syscall.EBADE {
t.Errorf("WrapErr: %v", err)
}
msg.PrintBaseErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:")
want := "\x00 \x00\ncannot cuddle cat: state not recoverable\n"
if buf.String() != want {
t.Errorf("WrapErr: %q, want %q", buf.String(), want)
}
})
t.Run("inactive", func(t *testing.T) {
{
inactive := msg.Resume()
if inactive {
t.Cleanup(func() { msg.Suspend() })
}
}
if msg.Resume() {
t.Error("Resume unexpected outcome")
}
msg.Suspend()
if !msg.Resume() {
t.Error("Resume unexpected outcome")
}
})
// the function is a noop
t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() })
}
type panicWriter struct{}
func (panicWriter) Write([]byte) (int, error) { panic("unreachable") }
func saveRestoreOutput(t *testing.T) {
out := container.GetOutput()
t.Cleanup(func() { container.SetOutput(out) })
}
func replaceOutput(t *testing.T) {
saveRestoreOutput(t)
container.SetOutput(&testOutput{t: t})
}
type testOutput struct {
t *testing.T
suspended atomic.Bool
}
func (out *testOutput) IsVerbose() bool { return testing.Verbose() }
func (out *testOutput) Verbose(v ...any) {
if !out.IsVerbose() {
return
}
out.t.Log(v...)
}
func (out *testOutput) Verbosef(format string, v ...any) {
if !out.IsVerbose() {
return
}
out.t.Logf(format, v...)
}
func (out *testOutput) WrapErr(err error, a ...any) error { return hlog.WrapErr(err, a...) }
func (out *testOutput) PrintBaseErr(err error, fallback string) { hlog.PrintBaseError(err, fallback) }
func (out *testOutput) Suspend() {
if out.suspended.CompareAndSwap(false, true) {
out.Verbose("suspend called")
return
}
out.Verbose("suspend called on suspended output")
}
func (out *testOutput) Resume() bool {
if out.suspended.CompareAndSwap(true, false) {
out.Verbose("resume called")
return true
}
out.Verbose("resume called on unsuspended output")
return false
}
func (out *testOutput) BeforeExit() { out.Verbose("beforeExit called") }

View File

@ -13,115 +13,56 @@ import (
"unsafe" "unsafe"
) )
const (
// intermediate root file name pattern for [MountOverlayOp.Upper];
// remains after apply returns
intermediatePatternOverlayUpper = "overlay.upper.*"
// intermediate root file name pattern for [MountOverlayOp.Work];
// remains after apply returns
intermediatePatternOverlayWork = "overlay.work.*"
// intermediate root file name pattern for [TmpfileOp]
intermediatePatternTmpfile = "tmp.*"
)
const (
nrAutoEtc = 1 << iota
nrAutoRoot
)
type ( type (
Ops []Op Ops []Op
Op interface {
// Op is a generic setup step ran inside the container init.
// Implementations of this interface are sent as a stream of gobs.
Op interface {
// early is called in host root. // early is called in host root.
early(state *setupState) error early(params *Params) error
// apply is called in intermediate root. // apply is called in intermediate root.
apply(state *setupState) error apply(params *Params) error
prefix() string prefix() string
Is(op Op) bool Is(op Op) bool
fmt.Stringer fmt.Stringer
} }
setupState struct {
nonrepeatable uintptr
*Params
}
) )
// Grow grows the slice Ops points to using [slices.Grow].
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) } func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
func init() { gob.Register(new(RemountOp)) }
// Remount appends an [Op] that applies [RemountOp.Flags] on container path [RemountOp.Target].
func (f *Ops) Remount(target *Absolute, flags uintptr) *Ops {
*f = append(*f, &RemountOp{target, flags})
return f
}
type RemountOp struct {
Target *Absolute
Flags uintptr
}
func (*RemountOp) early(*setupState) error { return nil }
func (r *RemountOp) apply(*setupState) error {
if r.Target == nil {
return EBADE
}
return wrapErrSuffix(hostProc.remount(toSysroot(r.Target.String()), r.Flags),
fmt.Sprintf("cannot remount %q:", r.Target))
}
func (r *RemountOp) Is(op Op) bool { vr, ok := op.(*RemountOp); return ok && *r == *vr }
func (*RemountOp) prefix() string { return "remounting" }
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }
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]. // BindMountOp bind mounts host path Source on container path Target.
func (f *Ops) Bind(source, target *Absolute, flags int) *Ops {
*f = append(*f, &BindMountOp{nil, source, target, flags})
return f
}
type BindMountOp struct { type BindMountOp struct {
sourceFinal, Source, Target *Absolute Source, SourceFinal, Target string
Flags int Flags int
} }
const ( const (
// BindOptional skips nonexistent host paths.
BindOptional = 1 << iota BindOptional = 1 << iota
// BindWritable mounts filesystem read-write.
BindWritable BindWritable
// BindDevice allows access to devices (special files) on this filesystem.
BindDevice BindDevice
) )
func (b *BindMountOp) early(*setupState) error { func (b *BindMountOp) early(*Params) error {
if b.Source == nil || b.Target == nil { if !path.IsAbs(b.Source) {
return EBADE return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", b.Source))
} }
if pathname, err := filepath.EvalSymlinks(b.Source.String()); err != nil { if v, err := filepath.EvalSymlinks(b.Source); err != nil {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 { if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
// leave sourceFinal as nil b.SourceFinal = "\x00"
return nil return nil
} }
return wrapErrSelf(err) return wrapErrSelf(err)
} else { } else {
b.sourceFinal, err = NewAbs(pathname) b.SourceFinal = v
return err return nil
} }
} }
func (b *BindMountOp) apply(*setupState) error { func (b *BindMountOp) apply(*Params) error {
if b.sourceFinal == nil { if b.SourceFinal == "\x00" {
if b.Flags&BindOptional == 0 { if b.Flags&BindOptional == 0 {
// unreachable // unreachable
return EBADE return EBADE
@ -129,8 +70,12 @@ func (b *BindMountOp) apply(*setupState) error {
return nil return nil
} }
source := toHost(b.sourceFinal.String()) if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) {
target := toSysroot(b.Target.String()) return msg.WrapErr(EBADE, "path is not absolute")
}
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
@ -152,7 +97,7 @@ func (b *BindMountOp) apply(*setupState) error {
flags |= MS_NODEV flags |= MS_NODEV
} }
return hostProc.bindMount(source, target, flags, b.sourceFinal == b.Target) return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target)
} }
func (b *BindMountOp) Is(op Op) bool { vb, ok := op.(*BindMountOp); return ok && *b == *vb } func (b *BindMountOp) Is(op Op) bool { vb, ok := op.(*BindMountOp); return ok && *b == *vb }
@ -161,80 +106,67 @@ func (b *BindMountOp) String() string {
if b.Source == b.Target { if b.Source == b.Target {
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags) return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
} }
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags) return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
}
func (f *Ops) Bind(source, target string, flags int) *Ops {
*f = append(*f, &BindMountOp{source, "", target, flags})
return f
} }
func init() { gob.Register(new(MountProcOp)) } func init() { gob.Register(new(MountProcOp)) }
// Proc appends an [Op] that mounts a private instance of proc. // MountProcOp mounts a private instance of proc.
func (f *Ops) Proc(target *Absolute) *Ops { type MountProcOp string
*f = append(*f, &MountProcOp{target})
return f
}
type MountProcOp struct { func (p MountProcOp) early(*Params) error { return nil }
Target *Absolute func (p MountProcOp) apply(params *Params) error {
} v := string(p)
func (p *MountProcOp) early(*setupState) error { return nil } if !path.IsAbs(v) {
func (p *MountProcOp) apply(state *setupState) error { return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
if p.Target == nil {
return EBADE
} }
target := toSysroot(p.Target.String())
if err := os.MkdirAll(target, state.ParentPerm); err != nil { target := toSysroot(v)
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("proc", target, "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""),
fmt.Sprintf("cannot mount proc on %q:", p.Target.String())) fmt.Sprintf("cannot mount proc on %q:", v))
} }
func (p *MountProcOp) Is(op Op) bool { func (p MountProcOp) Is(op Op) bool { vp, ok := op.(MountProcOp); return ok && p == vp }
vp, ok := op.(*MountProcOp) func (MountProcOp) prefix() string { return "mounting" }
return ok && ((p == nil && vp == nil) || p == vp) func (p MountProcOp) String() string { return fmt.Sprintf("proc on %q", string(p)) }
func (f *Ops) Proc(dest string) *Ops {
*f = append(*f, MountProcOp(dest))
return f
} }
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. // MountDevOp mounts part of host dev.
func (f *Ops) Dev(target *Absolute, mqueue bool) *Ops { type MountDevOp string
*f = append(*f, &MountDevOp{target, mqueue, false})
return f
}
// DevWritable appends an [Op] that mounts a writable subset of host /dev. func (d MountDevOp) early(*Params) error { return nil }
// There is usually no good reason to write to /dev, so this should always be followed by a [RemountOp]. func (d MountDevOp) apply(params *Params) error {
func (f *Ops) DevWritable(target *Absolute, mqueue bool) *Ops { v := string(d)
*f = append(*f, &MountDevOp{target, mqueue, true})
return f
}
type MountDevOp struct { if !path.IsAbs(v) {
Target *Absolute return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
Mqueue bool
Write bool
}
func (d *MountDevOp) early(*setupState) error { return nil }
func (d *MountDevOp) apply(state *setupState) error {
if d.Target == nil {
return EBADE
} }
target := toSysroot(d.Target.String()) target := toSysroot(v)
if err := mountTmpfs(SourceTmpfsDevtmpfs, target, MS_NOSUID|MS_NODEV, 0, state.ParentPerm); err != nil { if err := mountTmpfs("devtmpfs", v, 0, params.ParentPerm); err != nil {
return err return err
} }
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} { for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
targetPath := path.Join(target, name) targetPath := toSysroot(path.Join(v, name))
if err := ensureFile(targetPath, 0444, state.ParentPerm); err != nil { if err := ensureFile(targetPath, 0444, params.ParentPerm); err != nil {
return err return err
} }
if err := hostProc.bindMount( if err := hostProc.bindMount(
toHost(FHSDev+name), toHost("/dev/"+name),
targetPath, targetPath,
0, 0,
true, true,
@ -244,15 +176,15 @@ func (d *MountDevOp) apply(state *setupState) error {
} }
for i, name := range []string{"stdin", "stdout", "stderr"} { for i, name := range []string{"stdin", "stdout", "stderr"} {
if err := os.Symlink( if err := os.Symlink(
FHSProc+"self/fd/"+string(rune(i+'0')), "/proc/self/fd/"+string(rune(i+'0')),
path.Join(target, name), path.Join(target, name),
); err != nil { ); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} }
} }
for _, pair := range [][2]string{ for _, pair := range [][2]string{
{FHSProc + "self/fd", "fd"}, {"/proc/self/fd", "fd"},
{FHSProc + "kcore", "core"}, {"/proc/kcore", "core"},
{"pts/ptmx", "ptmx"}, {"pts/ptmx", "ptmx"},
} { } {
if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil { if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil {
@ -262,22 +194,22 @@ func (d *MountDevOp) apply(state *setupState) error {
devPtsPath := path.Join(target, "pts") devPtsPath := path.Join(target, "pts")
for _, name := range []string{path.Join(target, "shm"), devPtsPath} { for _, name := range []string{path.Join(target, "shm"), devPtsPath} {
if err := os.Mkdir(name, state.ParentPerm); err != nil { if err := os.Mkdir(name, params.ParentPerm); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} }
} }
if err := Mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC, if err := Mount("devpts", devPtsPath, "devpts", MS_NOSUID|MS_NOEXEC,
"newinstance,ptmxmode=0666,mode=620"); err != nil { "newinstance,ptmxmode=0666,mode=620"); err != nil {
return wrapErrSuffix(err, return wrapErrSuffix(err,
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath)) fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
} }
if state.RetainSession { if params.RetainSession {
var buf [8]byte var buf [8]byte
if _, _, errno := Syscall(SYS_IOCTL, 1, TIOCGWINSZ, uintptr(unsafe.Pointer(&buf[0]))); errno == 0 { if _, _, errno := Syscall(SYS_IOCTL, 1, TIOCGWINSZ, uintptr(unsafe.Pointer(&buf[0]))); errno == 0 {
consolePath := path.Join(target, "console") consolePath := toSysroot(path.Join(v, "console"))
if err := ensureFile(consolePath, 0444, state.ParentPerm); err != nil { if err := ensureFile(consolePath, 0444, params.ParentPerm); err != nil {
return err return err
} }
if name, err := os.Readlink(hostProc.stdout()); err != nil { if name, err := os.Readlink(hostProc.stdout()); err != nil {
@ -293,329 +225,161 @@ func (d *MountDevOp) apply(state *setupState) error {
} }
} }
if d.Mqueue { return nil
mqueueTarget := path.Join(target, "mqueue")
if err := os.Mkdir(mqueueTarget, state.ParentPerm); err != nil {
return wrapErrSelf(err)
}
if err := Mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil {
return wrapErrSuffix(err, "cannot mount mqueue:")
}
}
if d.Write {
return nil
}
return wrapErrSuffix(hostProc.remount(target, MS_RDONLY),
fmt.Sprintf("cannot remount %q:", target))
} }
func (d *MountDevOp) Is(op Op) bool { vd, ok := op.(*MountDevOp); return ok && *d == *vd } func (d MountDevOp) Is(op Op) bool { vd, ok := op.(MountDevOp); return ok && d == vd }
func (*MountDevOp) prefix() string { return "mounting" } func (MountDevOp) prefix() string { return "mounting" }
func (d *MountDevOp) String() string { func (d MountDevOp) String() string { return fmt.Sprintf("dev on %q", string(d)) }
if d.Mqueue { func (f *Ops) Dev(dest string) *Ops {
return fmt.Sprintf("dev on %q with mqueue", d.Target) *f = append(*f, MountDevOp(dest))
return f
}
func init() { gob.Register(new(MountMqueueOp)) }
// MountMqueueOp mounts a private mqueue instance on container Path.
type MountMqueueOp string
func (m MountMqueueOp) early(*Params) error { return nil }
func (m MountMqueueOp) apply(params *Params) error {
v := string(m)
if !path.IsAbs(v) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
} }
return fmt.Sprintf("dev on %q", d.Target)
target := toSysroot(v)
if err := os.MkdirAll(target, params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
return wrapErrSuffix(Mount("mqueue", target, "mqueue", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""),
fmt.Sprintf("cannot mount mqueue on %q:", v))
}
func (m MountMqueueOp) Is(op Op) bool { vm, ok := op.(MountMqueueOp); return ok && m == vm }
func (MountMqueueOp) prefix() string { return "mounting" }
func (m MountMqueueOp) String() string { return fmt.Sprintf("mqueue on %q", string(m)) }
func (f *Ops) Mqueue(dest string) *Ops {
*f = append(*f, MountMqueueOp(dest))
return f
} }
func init() { gob.Register(new(MountTmpfsOp)) } func init() { gob.Register(new(MountTmpfsOp)) }
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path]. // MountTmpfsOp mounts tmpfs on container Path.
func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm})
return f
}
// Readonly appends an [Op] that mounts read-only tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Readonly(target *Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, target, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm})
return f
}
type MountTmpfsOp struct { type MountTmpfsOp struct {
FSName string Path string
Path *Absolute Size int
Flags uintptr Perm os.FileMode
Size int
Perm os.FileMode
} }
func (t *MountTmpfsOp) early(*setupState) error { return nil } func (t *MountTmpfsOp) early(*Params) error { return nil }
func (t *MountTmpfsOp) apply(*setupState) error { func (t *MountTmpfsOp) apply(*Params) error {
if t.Path == nil { if !path.IsAbs(t.Path) {
return EBADE return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path))
} }
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.String()), t.Flags, t.Size, t.Perm) return mountTmpfs("tmpfs", t.Path, 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 }
func (*MountTmpfsOp) prefix() string { return "mounting" } func (*MountTmpfsOp) prefix() string { return "mounting" }
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) } func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
func init() { gob.Register(new(MountOverlayOp)) } *f = append(*f, &MountTmpfsOp{dest, size, perm})
// Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target].
func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops {
*f = append(*f, &MountOverlayOp{
Target: target,
Lower: layers,
Upper: state,
Work: work,
})
return f return f
} }
// OverlayEphemeral appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]
// with an ephemeral upperdir and workdir.
func (f *Ops) OverlayEphemeral(target *Absolute, layers ...*Absolute) *Ops {
return f.Overlay(target, AbsFHSRoot, nil, layers...)
}
// OverlayReadonly appends an [Op] that mounts the overlay pseudo filesystem readonly on [MountOverlayOp.Target]
func (f *Ops) OverlayReadonly(target *Absolute, layers ...*Absolute) *Ops {
return f.Overlay(target, nil, nil, layers...)
}
type MountOverlayOp struct {
Target *Absolute
// Any filesystem, does not need to be on a writable filesystem.
Lower []*Absolute
// formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early
lower []string
// The upperdir is normally on a writable filesystem.
//
// If Work is nil and Upper holds the special value [FHSRoot],
// an ephemeral upperdir and workdir will be set up.
//
// If both Work and Upper are empty strings, upperdir and workdir is omitted and the overlay is mounted readonly.
Upper *Absolute
// formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early
upper string
// The workdir needs to be an empty directory on the same filesystem as upperdir.
Work *Absolute
// formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early
work string
ephemeral bool
}
func (o *MountOverlayOp) early(*setupState) error {
if o.Work == nil && o.Upper != nil {
switch o.Upper.String() {
case FHSRoot: // ephemeral
o.ephemeral = true // intermediate root not yet available
default:
return msg.WrapErr(EINVAL, fmt.Sprintf("upperdir has unexpected value %q", o.Upper))
}
}
// readonly handled in apply
if !o.ephemeral {
if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) {
// unreachable
return msg.WrapErr(ENOTRECOVERABLE, "impossible overlay state reached")
}
if o.Upper != nil {
if v, err := filepath.EvalSymlinks(o.Upper.String()); err != nil {
return wrapErrSelf(err)
} else {
o.upper = EscapeOverlayDataSegment(toHost(v))
}
}
if o.Work != nil {
if v, err := filepath.EvalSymlinks(o.Work.String()); err != nil {
return wrapErrSelf(err)
} else {
o.work = EscapeOverlayDataSegment(toHost(v))
}
}
}
o.lower = make([]string, len(o.Lower))
for i, a := range o.Lower {
if a == nil {
return EBADE
}
if v, err := filepath.EvalSymlinks(a.String()); err != nil {
return wrapErrSelf(err)
} else {
o.lower[i] = EscapeOverlayDataSegment(toHost(v))
}
}
return nil
}
func (o *MountOverlayOp) apply(state *setupState) error {
if o.Target == nil {
return EBADE
}
target := toSysroot(o.Target.String())
if err := os.MkdirAll(target, state.ParentPerm); err != nil {
return wrapErrSelf(err)
}
if o.ephemeral {
var err error
// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
if o.upper, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil {
return wrapErrSelf(err)
}
if o.work, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil {
return wrapErrSelf(err)
}
}
options := make([]string, 0, 4)
if o.upper == zeroString && o.work == zeroString { // readonly
if len(o.Lower) < 2 {
return msg.WrapErr(EINVAL, "readonly overlay requires at least two lowerdir")
}
// "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only
} else {
if len(o.Lower) == 0 {
return msg.WrapErr(EINVAL, "overlay requires at least one lowerdir")
}
options = append(options,
OptionOverlayUpperdir+"="+o.upper,
OptionOverlayWorkdir+"="+o.work)
}
options = append(options,
OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath),
OptionOverlayUserxattr)
return wrapErrSuffix(Mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)),
fmt.Sprintf("cannot mount overlay on %q:", o.Target))
}
func (o *MountOverlayOp) Is(op Op) bool {
vo, ok := op.(*MountOverlayOp)
return ok &&
o.Target == vo.Target &&
slices.Equal(o.Lower, vo.Lower) &&
o.Upper == vo.Upper &&
o.Work == vo.Work
}
func (*MountOverlayOp) prefix() string { return "mounting" }
func (o *MountOverlayOp) String() string {
return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower))
}
func init() { gob.Register(new(SymlinkOp)) } func init() { gob.Register(new(SymlinkOp)) }
// Link appends an [Op] that creates a symlink in the container filesystem. // SymlinkOp creates a symlink in the container filesystem.
func (f *Ops) Link(target *Absolute, linkName string, dereference bool) *Ops { type SymlinkOp [2]string
*f = append(*f, &SymlinkOp{target, linkName, dereference})
return f
}
type SymlinkOp struct { func (l *SymlinkOp) early(*Params) error {
Target *Absolute if strings.HasPrefix(l[0], "*") {
// LinkName is an arbitrary uninterpreted pathname. l[0] = l[0][1:]
LinkName string if !path.IsAbs(l[0]) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[0]))
// Dereference causes LinkName to be dereferenced during early.
Dereference bool
}
func (l *SymlinkOp) early(*setupState) error {
if l.Dereference {
if !isAbs(l.LinkName) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l.LinkName))
} }
if name, err := os.Readlink(l.LinkName); err != nil { if name, err := os.Readlink(l[0]); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} else { } else {
l.LinkName = name l[0] = name
} }
} }
return nil return nil
} }
func (l *SymlinkOp) apply(params *Params) error {
func (l *SymlinkOp) apply(state *setupState) error { // symlink target is an arbitrary path value, so only validate link name here
if l.Target == nil { if !path.IsAbs(l[1]) {
return EBADE return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[1]))
} }
target := toSysroot(l.Target.String())
if err := os.MkdirAll(path.Dir(target), state.ParentPerm); err != nil { target := toSysroot(l[1])
if err := os.MkdirAll(path.Dir(target), params.ParentPerm); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} }
if err := os.Symlink(l.LinkName, target); err != nil { if err := os.Symlink(l[0], 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 { func (l *SymlinkOp) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) }
return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName) func (f *Ops) Link(target, linkName string) *Ops {
*f = append(*f, &SymlinkOp{target, linkName})
return f
} }
func init() { gob.Register(new(MkdirOp)) } func init() { gob.Register(new(MkdirOp)) }
// Mkdir appends an [Op] that creates a directory in the container filesystem. // MkdirOp creates a directory in the container filesystem.
func (f *Ops) Mkdir(name *Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{name, perm})
return f
}
type MkdirOp struct { type MkdirOp struct {
Path *Absolute Path string
Perm os.FileMode Perm os.FileMode
} }
func (m *MkdirOp) early(*setupState) error { return nil } func (m *MkdirOp) early(*Params) error { return nil }
func (m *MkdirOp) apply(*setupState) error { func (m *MkdirOp) apply(*Params) error {
if m.Path == nil { if !path.IsAbs(m.Path) {
return EBADE return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", m.Path))
} }
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 }
func (*MkdirOp) prefix() string { return "creating" } func (*MkdirOp) prefix() string { return "creating" }
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) } func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{dest, perm})
return f
}
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]. // TmpfileOp places a file in container Path containing Data.
func (f *Ops) Place(name *Absolute, data []byte) *Ops {
*f = append(*f, &TmpfileOp{name, data})
return f
}
// PlaceP is like Place but writes the address of [TmpfileOp.Data] to the pointer dataP points to.
func (f *Ops) PlaceP(name *Absolute, dataP **[]byte) *Ops {
t := &TmpfileOp{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}
type TmpfileOp struct { type TmpfileOp struct {
Path *Absolute Path string
Data []byte Data []byte
} }
func (t *TmpfileOp) early(*setupState) error { return nil } func (t *TmpfileOp) early(*Params) error { return nil }
func (t *TmpfileOp) apply(state *setupState) error { func (t *TmpfileOp) apply(params *Params) error {
if t.Path == nil { if !path.IsAbs(t.Path) {
return EBADE return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path))
} }
var tmpPath string var tmpPath string
if f, err := os.CreateTemp(FHSRoot, intermediatePatternTmpfile); err != nil { if f, err := os.CreateTemp("/", "tmp.*"); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} else if _, err = f.Write(t.Data); err != nil { } else if _, err = f.Write(t.Data); err != nil {
return wrapErrSuffix(err, return wrapErrSuffix(err,
@ -627,8 +391,8 @@ func (t *TmpfileOp) apply(state *setupState) error {
tmpPath = f.Name() tmpPath = f.Name()
} }
target := toSysroot(t.Path.String()) target := toSysroot(t.Path)
if err := ensureFile(target, 0444, state.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(
tmpPath, tmpPath,
@ -651,3 +415,68 @@ func (*TmpfileOp) prefix() string { return "placing" }
func (t *TmpfileOp) String() string { func (t *TmpfileOp) String() string {
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data)) return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
} }
func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &TmpfileOp{name, data}); return f }
func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
t := &TmpfileOp{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}
func init() { gob.Register(new(AutoEtcOp)) }
// AutoEtcOp 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.
type AutoEtcOp struct{ Prefix string }
func (e *AutoEtcOp) early(*Params) error { return nil }
func (e *AutoEtcOp) apply(*Params) error {
const target = sysrootPath + "/etc/"
rel := e.hostRel() + "/"
if err := os.MkdirAll(target, 0755); err != nil {
return wrapErrSelf(err)
}
if d, err := os.ReadDir(toSysroot(e.hostPath())); err != nil {
return wrapErrSelf(err)
} else {
for _, ent := range d {
n := ent.Name()
switch n {
case ".host":
case "passwd":
case "group":
case "mtab":
if err = os.Symlink("/proc/mounts", target+n); err != nil {
return wrapErrSelf(err)
}
default:
if err = os.Symlink(rel+n, target+n); err != nil {
return wrapErrSelf(err)
}
}
}
}
return nil
}
func (e *AutoEtcOp) hostPath() string { return "/etc/" + e.hostRel() }
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtcOp) Is(op Op) bool {
ve, ok := op.(*AutoEtcOp)
return ok && ((e == nil && ve == nil) || (e != nil && ve != nil && *e == *ve))
}
func (*AutoEtcOp) prefix() string { return "setting up" }
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
func (f *Ops) Etc(host, prefix string) *Ops {
e := &AutoEtcOp{prefix}
f.Mkdir("/etc", 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)
return f
}

View File

@ -1,110 +0,0 @@
package container
import (
"reflect"
"syscall"
"testing"
)
func TestGetSetOutput(t *testing.T) {
{
out := GetOutput()
t.Cleanup(func() { SetOutput(out) })
}
t.Run("default", func(t *testing.T) {
SetOutput(new(stubOutput))
if v, ok := GetOutput().(*DefaultMsg); ok {
t.Fatalf("SetOutput: got unexpected output %#v", v)
}
SetOutput(nil)
if _, ok := GetOutput().(*DefaultMsg); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
}
})
t.Run("stub", func(t *testing.T) {
SetOutput(new(stubOutput))
if _, ok := GetOutput().(*stubOutput); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
}
})
}
func TestWrapErr(t *testing.T) {
{
out := GetOutput()
t.Cleanup(func() { SetOutput(out) })
}
var wrapFp *func(error, ...any) error
s := new(stubOutput)
SetOutput(s)
wrapFp = &s.wrapF
testCases := []struct {
name string
f func(t *testing.T)
wantErr error
wantA []any
}{
{"suffix nil", func(t *testing.T) {
if err := wrapErrSuffix(nil, "\x00"); err != nil {
t.Errorf("wrapErrSuffix: %v", err)
}
}, nil, nil},
{"suffix val", func(t *testing.T) {
if err := wrapErrSuffix(syscall.ENOTRECOVERABLE, "\x00\x00"); err != syscall.ENOTRECOVERABLE {
t.Errorf("wrapErrSuffix: %v", err)
}
}, syscall.ENOTRECOVERABLE, []any{"\x00\x00", syscall.ENOTRECOVERABLE}},
{"self nil", func(t *testing.T) {
if err := wrapErrSelf(nil); err != nil {
t.Errorf("wrapErrSelf: %v", err)
}
}, nil, nil},
{"self val", func(t *testing.T) {
if err := wrapErrSelf(syscall.ENOTRECOVERABLE); err != syscall.ENOTRECOVERABLE {
t.Errorf("wrapErrSelf: %v", err)
}
}, syscall.ENOTRECOVERABLE, []any{"state not recoverable"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var (
gotErr error
gotA []any
)
*wrapFp = func(err error, a ...any) error { gotErr = err; gotA = a; return err }
tc.f(t)
if gotErr != tc.wantErr {
t.Errorf("WrapErr: err = %v, want %v", gotErr, tc.wantErr)
}
if !reflect.DeepEqual(gotA, tc.wantA) {
t.Errorf("WrapErr: a = %v, want %v", gotA, tc.wantA)
}
})
}
}
type stubOutput struct {
wrapF func(error, ...any) error
}
func (*stubOutput) IsVerbose() bool { panic("unreachable") }
func (*stubOutput) Verbose(...any) { panic("unreachable") }
func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") }
func (*stubOutput) PrintBaseErr(error, string) { panic("unreachable") }
func (*stubOutput) Suspend() { panic("unreachable") }
func (*stubOutput) Resume() bool { panic("unreachable") }
func (*stubOutput) BeforeExit() { panic("unreachable") }
func (s *stubOutput) WrapErr(err error, v ...any) error {
if s.wrapF == nil {
panic("unreachable")
}
return s.wrapF(err, v...)
}

View File

@ -5,11 +5,11 @@ import (
"errors" "errors"
"os" "os"
"strconv" "strconv"
"syscall"
) )
var ( var (
ErrNotSet = errors.New("environment variable not set") ErrNotSet = errors.New("environment variable not set")
ErrInvalid = errors.New("bad file descriptor")
) )
// Setup appends the read end of a pipe for setup params transmission and returns its fd. // Setup appends the read end of a pipe for setup params transmission and returns its fd.
@ -35,7 +35,7 @@ func Receive(key string, e any, v **os.File) (func() error, error) {
} else { } else {
setup = os.NewFile(uintptr(fd), "setup") setup = os.NewFile(uintptr(fd), "setup")
if setup == nil { if setup == nil {
return nil, syscall.EBADF return nil, ErrInvalid
} }
if v != nil { if v != nil {
*v = setup *v = setup

View File

@ -13,81 +13,10 @@ 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. hostPath = "/" + hostDir
FHSRoot = "/"
// FHSEtc points to the directory for system-specific configuration.
FHSEtc = "/etc/"
// FHSTmp points to the place for small temporary files.
FHSTmp = "/tmp/"
// FHSRun points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar.
FHSRun = "/run/"
// FHSRunUser points to a directory containing per-user runtime directories,
// each usually individually mounted "tmpfs" instances.
FHSRunUser = FHSRun + "user/"
// FHSUsr points to vendor-supplied operating system resources.
FHSUsr = "/usr/"
// FHSUsrBin points to binaries and executables for user commands that shall appear in the $PATH search path.
FHSUsrBin = FHSUsr + "bin/"
// FHSVar points to persistent, variable system data. Writable during normal system operation.
FHSVar = "/var/"
// FHSVarLib points to persistent system data.
FHSVarLib = FHSVar + "lib/"
// FHSVarEmpty points to a nonstandard directory that is usually empty.
FHSVarEmpty = FHSVar + "empty/"
// FHSDev points to the root directory for device nodes.
FHSDev = "/dev/"
// FHSProc points to a virtual kernel file system exposing the process list and other functionality.
FHSProc = "/proc/"
// FHSProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
FHSProcSys = FHSProc + "sys/"
// FHSSys points to a virtual kernel file system exposing discovered devices and other functionality.
FHSSys = "/sys/"
)
var (
// AbsFHSRoot is [FHSRoot] as [Absolute].
AbsFHSRoot = &Absolute{FHSRoot}
// AbsFHSEtc is [FHSEtc] as [Absolute].
AbsFHSEtc = &Absolute{FHSEtc}
// AbsFHSTmp is [FHSTmp] as [Absolute].
AbsFHSTmp = &Absolute{FHSTmp}
// AbsFHSRun is [FHSRun] as [Absolute].
AbsFHSRun = &Absolute{FHSRun}
// AbsFHSRunUser is [FHSRunUser] as [Absolute].
AbsFHSRunUser = &Absolute{FHSRunUser}
// AbsFHSUsrBin is [FHSUsrBin] as [Absolute].
AbsFHSUsrBin = &Absolute{FHSUsrBin}
// AbsFHSVar is [FHSVar] as [Absolute].
AbsFHSVar = &Absolute{FHSVar}
// AbsFHSVarLib is [FHSVarLib] as [Absolute].
AbsFHSVarLib = &Absolute{FHSVarLib}
// AbsFHSDev is [FHSDev] as [Absolute].
AbsFHSDev = &Absolute{FHSDev}
// AbsFHSProc is [FHSProc] as [Absolute].
AbsFHSProc = &Absolute{FHSProc}
// AbsFHSSys is [FHSSys] as [Absolute].
AbsFHSSys = &Absolute{FHSSys}
)
const (
// Nonexistent is a path that cannot exist.
// /proc is chosen because a system with covered /proc is unsupported by this package.
Nonexistent = FHSProc + "nonexistent"
hostPath = FHSRoot + hostDir
hostDir = "host" hostDir = "host"
sysrootPath = FHSRoot + sysrootDir sysrootPath = "/" + sysrootDir
sysrootDir = "sysroot" sysrootDir = "sysroot"
) )
@ -134,9 +63,9 @@ func ensureFile(name string, perm, pperm os.FileMode) error {
return err return err
} }
var hostProc = newProcPaths(hostPath) var hostProc = newProcPats(hostPath)
func newProcPaths(prefix string) *procPaths { func newProcPats(prefix string) *procPaths {
return &procPaths{prefix + "/proc", prefix + "/proc/self"} return &procPaths{prefix + "/proc", prefix + "/proc/self"}
} }

View File

@ -1,42 +0,0 @@
package container
import "testing"
func TestToSysroot(t *testing.T) {
testCases := []struct {
name string
want string
}{
{"", "/sysroot"},
{"/", "/sysroot"},
{"//etc///", "/sysroot/etc"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := toSysroot(tc.name); got != tc.want {
t.Errorf("toSysroot: %q, want %q", got, tc.want)
}
})
}
}
func TestToHost(t *testing.T) {
testCases := []struct {
name string
want string
}{
{"", "/host"},
{"/", "/host"},
{"//etc///", "/host/etc"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := toHost(tc.name); got != tc.want {
t.Errorf("toHost: %q, want %q", got, tc.want)
}
})
}
}
// InternalToHostOvlEscape exports toHost passed to EscapeOverlayDataSegment.
func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment(toHost(s)) }

View File

@ -79,7 +79,7 @@ func TestExport(t *testing.T) {
func BenchmarkExport(b *testing.B) { func BenchmarkExport(b *testing.B) {
buf := make([]byte, 8) buf := make([]byte, 8)
for b.Loop() { for i := 0; i < b.N; i++ {
e := New( e := New(
Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32, Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32,
AllowMultiarch|AllowCAN|AllowBluetooth), AllowMultiarch|AllowCAN|AllowBluetooth),

View File

@ -17,9 +17,9 @@ var (
) )
const ( const (
kernelOverflowuidPath = FHSProcSys + "kernel/overflowuid" kernelOverflowuidPath = "/proc/sys/kernel/overflowuid"
kernelOverflowgidPath = FHSProcSys + "kernel/overflowgid" kernelOverflowgidPath = "/proc/sys/kernel/overflowgid"
kernelCapLastCapPath = FHSProcSys + "kernel/cap_last_cap" kernelCapLastCapPath = "/proc/sys/kernel/cap_last_cap"
) )
func mustReadSysctl() { func mustReadSysctl() {

2
dist/install.sh vendored
View File

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

12
flake.lock generated
View File

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1753479839, "lastModified": 1748665073,
"narHash": "sha256-E/rPVh7vyPMJUFl2NAew+zibNGfVbANr8BP8nLRbLkQ=", "narHash": "sha256-RMhjnPKWtCoIIHiuR9QKD7xfsKb3agxzMfJY8V9MOew=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "0b9bf983db4d064764084cd6748efb1ab8297d1e", "rev": "282e1e029cb6ab4811114fc85110613d72771dea",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -23,11 +23,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1753345091, "lastModified": 1749024892,
"narHash": "sha256-CdX2Rtvp5I8HGu9swBmYuq+ILwRxpXdJwlpg8jvN4tU=", "narHash": "sha256-OGcDEz60TXQC+gVz5sdtgGJdKVYr6rwdzQKuZAJQpCA=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3ff0e34b1383648053bba8ed03f201d3466f90c9", "rev": "8f1b52b04f2cb6e5ead50bd28d76528a2f0380ef",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -32,7 +32,7 @@
buildPackage = forAllSystems ( buildPackage = forAllSystems (
system: system:
nixpkgsFor.${system}.callPackage ( nixpkgsFor.${system}.callPackage (
import ./cmd/hpkg/build.nix { import ./cmd/planterette/build.nix {
inherit inherit
nixpkgsFor nixpkgsFor
system system
@ -69,7 +69,7 @@
withRace = true; withRace = true;
}; };
hpkg = callPackage ./cmd/hpkg/test { inherit system self; }; planterette = callPackage ./cmd/planterette/test { inherit system self; };
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } '' formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
cd ${./.} cd ${./.}
@ -125,7 +125,7 @@
glibc glibc
xdg-dbus-proxy xdg-dbus-proxy
# hpkg # planterette
zstd zstd
gnutar gnutar
coreutils coreutils
@ -159,54 +159,6 @@
default = pkgs.mkShell { buildInputs = hakurei.targetPkgs; }; default = pkgs.mkShell { buildInputs = hakurei.targetPkgs; };
withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; }; withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
vm =
let
nixos = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
{
environment = {
systemPackages = [
(pkgs.buildFHSEnv {
pname = "hakurei-fhs";
inherit (hakurei) version;
targetPkgs = _: hakurei.targetPkgs;
extraOutputsToInstall = [ "dev" ];
profile = ''
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
'';
})
];
hakurei =
let
# this is used for interactive vm testing during development, where tests might be broken
package = self.packages.${pkgs.system}.hakurei.override {
buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; });
};
in
{
inherit package;
hsuPackage = self.packages.${pkgs.system}.hsu.override { hakurei = package; };
};
};
}
./test/interactive/configuration.nix
./test/interactive/vm.nix
./test/interactive/hakurei.nix
./test/interactive/trace.nix
self.nixosModules.hakurei
self.inputs.home-manager.nixosModules.home-manager
];
};
in
pkgs.mkShell {
buildInputs = [ nixos.config.system.build.vm ];
shellHook = "exec run-nixos-vm $@";
};
generateDoc = generateDoc =
let let
inherit (pkgs) lib; inherit (pkgs) lib;

View File

@ -8,13 +8,12 @@ import (
"os/exec" "os/exec"
"testing" "testing"
"hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
) )
func TestCmd(t *testing.T) { func TestCmd(t *testing.T) {
t.Run("start non-existent helper path", func(t *testing.T) { t.Run("start non-existent helper path", func(t *testing.T) {
h := helper.NewDirect(t.Context(), container.Nonexistent, argsWt, false, argF, nil, nil) h := helper.NewDirect(t.Context(), "/proc/nonexistent", argsWt, false, argF, nil, nil)
if err := h.Start(); !errors.Is(err, os.ErrNotExist) { if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
t.Errorf("Start: error = %v, wantErr %v", t.Errorf("Start: error = %v, wantErr %v",

View File

@ -16,7 +16,7 @@ import (
// New initialises a Helper instance with wt as the null-terminated argument writer. // New initialises a Helper instance with wt as the null-terminated argument writer.
func New( func New(
ctx context.Context, ctx context.Context,
pathname *container.Absolute, name string, name string,
wt io.WriterTo, wt io.WriterTo,
stat bool, stat bool,
argF func(argsFd, statFd int) []string, argF func(argsFd, statFd int) []string,
@ -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, name, args...) h.Container = container.New(ctx, name, args...)
h.WaitDelay = WaitDelay h.WaitDelay = WaitDelay
if cmdF != nil { if cmdF != nil {
cmdF(h.Container) cmdF(h.Container)

View File

@ -4,17 +4,20 @@ import (
"context" "context"
"io" "io"
"os" "os"
"os/exec"
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
) )
func TestContainer(t *testing.T) { func TestContainer(t *testing.T) {
t.Run("start empty container", func(t *testing.T) { t.Run("start empty container", func(t *testing.T) {
h := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil) h := helper.New(t.Context(), "/nonexistent", argsWt, false, argF, nil, nil)
wantErr := "container: starting an empty container" wantErr := "sandbox: starting an empty container"
if err := h.Start(); err == nil || err.Error() != wantErr { if err := h.Start(); err == nil || err.Error() != wantErr {
t.Errorf("Start: error = %v, wantErr %q", t.Errorf("Start: error = %v, wantErr %q",
err, wantErr) err, wantErr)
@ -22,7 +25,7 @@ func TestContainer(t *testing.T) {
}) })
t.Run("valid new helper nil check", func(t *testing.T) { t.Run("valid new helper nil check", func(t *testing.T) {
if got := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil); got == nil { if got := helper.New(t.Context(), "hakurei", argsWt, false, argF, nil, nil); got == nil {
t.Errorf("New(%q, %q) got nil", t.Errorf("New(%q, %q) got nil",
argsWt, "hakurei") argsWt, "hakurei")
return return
@ -31,13 +34,22 @@ func TestContainer(t *testing.T) {
t.Run("implementation compliance", func(t *testing.T) { t.Run("implementation compliance", func(t *testing.T) {
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper { 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, os.Args[0], argsWt, stat, argF, func(z *container.Container) {
setOutput(&z.Stdout, &z.Stderr) setOutput(&z.Stdout, &z.Stderr)
z. z.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
Bind(container.AbsFHSRoot, container.AbsFHSRoot, 0). return exec.CommandContext(ctx, os.Args[0], "-test.v",
Proc(container.AbsFHSProc). "-test.run=TestHelperInit", "--", "init")
Dev(container.AbsFHSDev, true) }
z.Bind("/", "/", 0).Proc("/proc").Dev("/dev")
}, nil) }, nil)
}) })
}) })
} }
func TestHelperInit(t *testing.T) {
if len(os.Args) != 5 || os.Args[4] != "init" {
return
}
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, func(bool) { internal.InstallOutput(false) })
}

View File

@ -38,6 +38,7 @@ func argF(argsFd, statFd int) []string {
func argFChecked(argsFd, statFd int) (args []string) { func argFChecked(argsFd, statFd int) (args []string) {
args = make([]string, 0, 6) args = make([]string, 0, 6)
args = append(args, "-test.run=TestHelperStub", "--")
if argsFd > -1 { if argsFd > -1 {
args = append(args, "--args", strconv.Itoa(argsFd)) args = append(args, "--args", strconv.Itoa(argsFd))
} }

View File

@ -25,7 +25,7 @@ func InternalHelperStub() {
sp = v sp = v
} }
genericStub(flagRestoreFiles(1, ap, sp)) genericStub(flagRestoreFiles(3, ap, sp))
os.Exit(0) os.Exit(0)
} }

View File

@ -1,17 +1,9 @@
package helper_test package helper_test
import ( import (
"os"
"testing" "testing"
"hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
) )
func TestMain(m *testing.M) { func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
helper.InternalHelperStub()
os.Exit(m.Run())
}

View File

@ -1,144 +1,75 @@
// Package hst exports shared types for invoking hakurei.
package hst package hst
import ( import (
"time" "hakurei.app/system"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"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 ( type Config struct {
Config struct { // reverse-DNS style arbitrary identifier string from config;
// reverse-DNS style arbitrary identifier string from config; // passed to wayland security-context-v1 as application ID
// passed to wayland security-context-v1 as application ID // and used as part of defaults in dbus session proxy
// and used as part of defaults in dbus session proxy ID string `json:"id"`
ID string `json:"id"`
// absolute path to executable file // absolute path to executable file
Path *container.Absolute `json:"path,omitempty"` Path string `json:"path,omitempty"`
// final args passed to container init // final args passed to container init
Args []string `json:"args"` Args []string `json:"args"`
// system services to make available in the container // system services to make available in the container
Enablements *Enablements `json:"enablements,omitempty"` Enablements system.Enablement `json:"enablements"`
// session D-Bus proxy configuration; // session D-Bus proxy configuration;
// nil makes session bus proxy assume built-in defaults // nil makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"` SessionBus *dbus.Config `json:"session_bus,omitempty"`
// system D-Bus proxy configuration; // system D-Bus proxy configuration;
// nil disables system bus proxy // nil disables system bus proxy
SystemBus *dbus.Config `json:"system_bus,omitempty"` SystemBus *dbus.Config `json:"system_bus,omitempty"`
// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1 // direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
// and the bare socket is mounted to the sandbox // and the bare socket is mounted to the sandbox
DirectWayland bool `json:"direct_wayland,omitempty"` DirectWayland bool `json:"direct_wayland,omitempty"`
// 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 // absolute path to shell, empty for host shell
Shell *container.Absolute `json:"shell"` Shell string `json:"shell,omitempty"`
// absolute path to home directory in the init mount namespace // absolute path to home directory in the init mount namespace
Data *container.Absolute `json:"data"` Data string `json:"data"`
// directory to enter and use as home in the container mount namespace, nil for Data // directory to enter and use as home in the container mount namespace, empty for Data
Dir *container.Absolute `json:"dir,omitempty"` Dir string `json:"dir"`
// 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"`
// numerical application id, used for init user namespace credentials // numerical application id, used for init user namespace credentials
Identity int `json:"identity"` Identity int `json:"identity"`
// list of supplementary groups inherited by container processes // list of supplementary groups inherited by container processes
Groups []string `json:"groups"` Groups []string `json:"groups"`
// abstract container configuration baseline // abstract container configuration baseline
Container *ContainerConfig `json:"container"` Container *ContainerConfig `json:"container"`
} }
// ContainerConfig describes the container configuration baseline to which the app implementation adds upon.
ContainerConfig struct {
// container hostname
Hostname string `json:"hostname,omitempty"`
// duration to wait for after interrupting a container's initial process in nanoseconds;
// a negative value causes the container to be terminated immediately on cancellation
WaitDelay time.Duration `json:"wait_delay,omitempty"`
// extra seccomp flags
SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"`
// extra seccomp presets
SeccompPresets seccomp.FilterPreset `json:"seccomp_presets"`
// disable project-specific filter extensions
SeccompCompat bool `json:"seccomp_compat,omitempty"`
// allow ptrace and friends
Devel bool `json:"devel,omitempty"`
// allow userns creation in container
Userns bool `json:"userns,omitempty"`
// share host net namespace
Net bool `json:"net,omitempty"`
// disallow accessing abstract UNIX domain sockets created outside the container
ScopeAbstract bool `json:"scope_abstract,omitempty"`
// allow dangerous terminal I/O
Tty bool `json:"tty,omitempty"`
// allow multiarch
Multiarch bool `json:"multiarch,omitempty"`
// initial process environment variables
Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"`
// pass through all devices
Device bool `json:"device,omitempty"`
// container mount points
Filesystem []FilesystemConfigJSON `json:"filesystem"`
// create symlinks inside container filesystem
Link []LinkConfig `json:"symlink"`
// automatically bind mount top-level directories to container root;
// the zero value disables this behaviour
AutoRoot *container.Absolute `json:"auto_root,omitempty"`
// extra flags for AutoRoot
RootFlags int `json:"root_flags,omitempty"`
// read-only /etc directory
Etc *container.Absolute `json:"etc,omitempty"`
// automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"`
}
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"`
}
)
// 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 *container.Absolute `json:"path"` Path string `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 {
if e == nil || e.Path == nil { buf := make([]byte, 0, 5+len(e.Path))
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.String())...) buf = append(buf, []byte(e.Path)...)
if e.Read { if e.Read {
buf[0] = 'r' buf[0] = 'r'
} }

View File

@ -1,35 +0,0 @@
package hst_test
import (
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestExtraPermConfig(t *testing.T) {
testCases := []struct {
name string
config *hst.ExtraPermConfig
want string
}{
{"nil", nil, "<invalid>"},
{"nil path", &hst.ExtraPermConfig{Path: nil}, "<invalid>"},
{"r", &hst.ExtraPermConfig{Path: container.AbsFHSRoot, Read: true}, "r--:/"},
{"r+", &hst.ExtraPermConfig{Ensure: true, Path: container.AbsFHSRoot, Read: true}, "r--+:/"},
{"w", &hst.ExtraPermConfig{Path: hst.AbsTmp, Write: true}, "-w-:/.hakurei"},
{"w+", &hst.ExtraPermConfig{Ensure: true, Path: hst.AbsTmp, Write: true}, "-w-+:/.hakurei"},
{"x", &hst.ExtraPermConfig{Path: container.AbsFHSRunUser, Execute: true}, "--x:/run/user/"},
{"x+", &hst.ExtraPermConfig{Ensure: true, Path: container.AbsFHSRunUser, Execute: true}, "--x+:/run/user/"},
{"rwx", &hst.ExtraPermConfig{Path: container.AbsFHSTmp, Read: true, Write: true, Execute: true}, "rwx:/tmp/"},
{"rwx+", &hst.ExtraPermConfig{Ensure: true, Path: container.AbsFHSTmp, Read: true, Write: true, Execute: true}, "rwx+:/tmp/"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.config.String(); got != tc.want {
t.Errorf("String: %q, want %q", got, tc.want)
}
})
}
}

65
hst/container.go Normal file
View File

@ -0,0 +1,65 @@
package hst
import (
"hakurei.app/container/seccomp"
)
type (
// ContainerConfig describes the container configuration baseline to which the app implementation adds upon.
ContainerConfig struct {
// container hostname
Hostname string `json:"hostname,omitempty"`
// extra seccomp flags
SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"`
// extra seccomp presets
SeccompPresets seccomp.FilterPreset `json:"seccomp_presets"`
// disable project-specific filter extensions
SeccompCompat bool `json:"seccomp_compat,omitempty"`
// allow ptrace and friends
Devel bool `json:"devel,omitempty"`
// allow userns creation in container
Userns bool `json:"userns,omitempty"`
// share host net namespace
Net bool `json:"net,omitempty"`
// disallow accessing abstract UNIX domain sockets created outside the container
ScopeAbstract bool `json:"scope_abstract,omitempty"`
// allow dangerous terminal I/O
Tty bool `json:"tty,omitempty"`
// allow multiarch
Multiarch bool `json:"multiarch,omitempty"`
// initial process environment variables
Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"`
// pass through all devices
Device bool `json:"device,omitempty"`
// container host filesystem bind mounts
Filesystem []*FilesystemConfig `json:"filesystem"`
// create symlinks inside container filesystem
Link [][2]string `json:"symlink"`
// read-only /etc directory
Etc string `json:"etc,omitempty"`
// automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"`
// cover these paths or create them if they do not already exist
Cover []string `json:"cover"`
}
// FilesystemConfig is an abstract representation of a bind mount.
FilesystemConfig struct {
// mount point in container, same as src if empty
Dst string `json:"dst,omitempty"`
// host filesystem path to make available to the container
Src string `json:"src"`
// do not mount filesystem read-only
Write bool `json:"write,omitempty"`
// do not disable device files
Device bool `json:"dev,omitempty"`
// fail if the bind mount cannot be established for any reason
Must bool `json:"require,omitempty"`
}
)

View File

@ -1,69 +0,0 @@
package hst
import (
"encoding/json"
"syscall"
"hakurei.app/system"
)
// NewEnablements returns the address of [system.Enablement] as [Enablements].
func NewEnablements(e system.Enablement) *Enablements { return (*Enablements)(&e) }
// enablementsJSON is the [json] representation of the [system.Enablement] bit field.
type enablementsJSON struct {
Wayland bool `json:"wayland,omitempty"`
X11 bool `json:"x11,omitempty"`
DBus bool `json:"dbus,omitempty"`
Pulse bool `json:"pulse,omitempty"`
}
// Enablements is the [json] adapter for [system.Enablement].
type Enablements system.Enablement
// Unwrap returns the underlying [system.Enablement].
func (e *Enablements) Unwrap() system.Enablement {
if e == nil {
return 0
}
return system.Enablement(*e)
}
func (e *Enablements) MarshalJSON() ([]byte, error) {
if e == nil {
return nil, syscall.EINVAL
}
return json.Marshal(&enablementsJSON{
Wayland: system.Enablement(*e)&system.EWayland != 0,
X11: system.Enablement(*e)&system.EX11 != 0,
DBus: system.Enablement(*e)&system.EDBus != 0,
Pulse: system.Enablement(*e)&system.EPulse != 0,
})
}
func (e *Enablements) UnmarshalJSON(data []byte) error {
if e == nil {
return syscall.EINVAL
}
v := new(enablementsJSON)
if err := json.Unmarshal(data, &v); err != nil {
return err
}
var ve system.Enablement
if v.Wayland {
ve |= system.EWayland
}
if v.X11 {
ve |= system.EX11
}
if v.DBus {
ve |= system.EDBus
}
if v.Pulse {
ve |= system.EPulse
}
*e = Enablements(ve)
return nil
}

View File

@ -1,108 +0,0 @@
package hst_test
import (
"encoding/json"
"errors"
"syscall"
"testing"
"hakurei.app/hst"
"hakurei.app/system"
)
func TestEnablements(t *testing.T) {
testCases := []struct {
name string
e *hst.Enablements
data string
sData string
}{
{"nil", nil, "null", `{"value":null,"magic":3236757504}`},
{"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`},
{"wayland", hst.NewEnablements(system.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
{"x11", hst.NewEnablements(system.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
{"dbus", hst.NewEnablements(system.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
{"pulse", hst.NewEnablements(system.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
{"all", hst.NewEnablements(system.EWayland | system.EX11 | system.EDBus | system.EPulse), `{"wayland":true,"x11":true,"dbus":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pulse":true},"magic":3236757504}`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("marshal", func(t *testing.T) {
if got, err := json.Marshal(tc.e); err != nil {
t.Fatalf("Marshal: error = %v", err)
} else if string(got) != tc.data {
t.Errorf("Marshal:\n%s, want\n%s", string(got), tc.data)
}
if got, err := json.Marshal(struct {
Value *hst.Enablements `json:"value"`
Magic int `json:"magic"`
}{tc.e, syscall.MS_MGC_VAL}); err != nil {
t.Fatalf("Marshal: error = %v", err)
} else if string(got) != tc.sData {
t.Errorf("Marshal:\n%s, want\n%s", string(got), tc.sData)
}
})
t.Run("unmarshal", func(t *testing.T) {
{
got := new(hst.Enablements)
if err := json.Unmarshal([]byte(tc.data), &got); err != nil {
t.Fatalf("Unmarshal: error = %v", err)
}
if tc.e == nil {
if got != nil {
t.Errorf("Unmarshal: %v", got)
}
} else if *got != *tc.e {
t.Errorf("Unmarshal: %v, want %v", got, tc.e)
}
}
{
got := *(new(struct {
Value *hst.Enablements `json:"value"`
Magic int `json:"magic"`
}))
if err := json.Unmarshal([]byte(tc.sData), &got); err != nil {
t.Fatalf("Unmarshal: error = %v", err)
}
if tc.e == nil {
if got.Value != nil {
t.Errorf("Unmarshal: %v", got)
}
} else if *got.Value != *tc.e {
t.Errorf("Unmarshal: %v, want %v", got.Value, tc.e)
}
}
})
})
}
t.Run("unwrap", func(t *testing.T) {
t.Run("nil", func(t *testing.T) {
if got := (*hst.Enablements)(nil).Unwrap(); got != 0 {
t.Errorf("Unwrap: %v", got)
}
})
t.Run("val", func(t *testing.T) {
if got := hst.NewEnablements(system.EWayland | system.EPulse).Unwrap(); got != system.EWayland|system.EPulse {
t.Errorf("Unwrap: %v", got)
}
})
})
t.Run("passthrough", func(t *testing.T) {
if _, err := (*hst.Enablements)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("MarshalJSON: error = %v", err)
}
if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) {
t.Errorf("UnmarshalJSON: error = %v", err)
}
if err := new(hst.Enablements).UnmarshalJSON([]byte{}); err == nil {
t.Errorf("UnmarshalJSON: error = %v", err)
}
})
}

120
hst/fs.go
View File

@ -1,120 +0,0 @@
package hst
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"hakurei.app/container"
)
// FilesystemConfig is an abstract representation of a mount point.
type FilesystemConfig interface {
// Valid returns whether the configuration is valid.
Valid() bool
// Path returns the target path in the container.
Path() *container.Absolute
// Host returns a slice of all host paths used by this operation.
Host() []*container.Absolute
// Apply appends the [container.Op] implementing this operation.
Apply(ops *container.Ops)
fmt.Stringer
}
var (
ErrFSNull = errors.New("unexpected null in mount point")
)
// FSTypeError is returned when [ContainerConfig.Filesystem] contains an entry with invalid type.
type FSTypeError string
func (f FSTypeError) Error() string { return fmt.Sprintf("invalid filesystem type %q", string(f)) }
// FSImplError is returned for unsupported implementations of [FilesystemConfig].
type FSImplError struct{ Value FilesystemConfig }
func (f FSImplError) Error() string {
implType := reflect.TypeOf(f.Value)
var name string
for implType != nil && implType.Kind() == reflect.Ptr {
name += "*"
implType = implType.Elem()
}
if implType != nil {
name += implType.Name()
} else {
name += "nil"
}
return fmt.Sprintf("implementation %s not supported", name)
}
// FilesystemConfigJSON is the [json] adapter for [FilesystemConfig].
type FilesystemConfigJSON struct{ FilesystemConfig }
// Valid returns whether the [FilesystemConfigJSON] is valid.
func (f *FilesystemConfigJSON) Valid() bool {
return f != nil && f.FilesystemConfig != nil && f.FilesystemConfig.Valid()
}
// fsType holds the string representation of a [FilesystemConfig]'s concrete type.
type fsType struct {
Type string `json:"type"`
}
func (f *FilesystemConfigJSON) MarshalJSON() ([]byte, error) {
if f == nil || f.FilesystemConfig == nil {
return nil, ErrFSNull
}
var v any
switch cv := f.FilesystemConfig.(type) {
case *FSBind:
v = &struct {
fsType
*FSBind
}{fsType{FilesystemBind}, cv}
case *FSEphemeral:
v = &struct {
fsType
*FSEphemeral
}{fsType{FilesystemEphemeral}, cv}
case *FSOverlay:
v = &struct {
fsType
*FSOverlay
}{fsType{FilesystemOverlay}, cv}
default:
return nil, FSImplError{f.FilesystemConfig}
}
return json.Marshal(v)
}
func (f *FilesystemConfigJSON) UnmarshalJSON(data []byte) error {
t := new(fsType)
if err := json.Unmarshal(data, &t); err != nil {
return err
}
if t == nil {
return ErrFSNull
}
switch t.Type {
case FilesystemBind:
*f = FilesystemConfigJSON{new(FSBind)}
case FilesystemEphemeral:
*f = FilesystemConfigJSON{new(FSEphemeral)}
case FilesystemOverlay:
*f = FilesystemConfigJSON{new(FSOverlay)}
default:
return FSTypeError(t.Type)
}
return json.Unmarshal(data, f.FilesystemConfig)
}

View File

@ -1,287 +0,0 @@
package hst_test
import (
"encoding/json"
"errors"
"reflect"
"strings"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestFilesystemConfigJSON(t *testing.T) {
testCases := []struct {
name string
want hst.FilesystemConfigJSON
wantErr error
data, sData string
}{
{"nil", hst.FilesystemConfigJSON{FilesystemConfig: nil}, hst.ErrFSNull,
`null`, `{"fs":null,"magic":3236757504}`},
{"bad type", hst.FilesystemConfigJSON{FilesystemConfig: stubFS{"cat"}},
hst.FSTypeError("cat"),
`{"type":"cat","meow":true}`, `{"fs":{"type":"cat","meow":true},"magic":3236757504}`},
{"bad impl bind", hst.FilesystemConfigJSON{FilesystemConfig: stubFS{"bind"}},
hst.FSImplError{Value: stubFS{"bind"}},
"\x00", "\x00"},
{"bad impl ephemeral", hst.FilesystemConfigJSON{FilesystemConfig: stubFS{"ephemeral"}},
hst.FSImplError{Value: stubFS{"ephemeral"}},
"\x00", "\x00"},
{"bad impl overlay", hst.FilesystemConfigJSON{FilesystemConfig: stubFS{"overlay"}},
hst.FSImplError{Value: stubFS{"overlay"}},
"\x00", "\x00"},
{"bind", hst.FilesystemConfigJSON{
FilesystemConfig: &hst.FSBind{
Target: m("/etc"),
Source: m("/mnt/etc"),
Optional: true,
},
}, nil,
`{"type":"bind","dst":"/etc","src":"/mnt/etc","optional":true}`,
`{"fs":{"type":"bind","dst":"/etc","src":"/mnt/etc","optional":true},"magic":3236757504}`},
{"ephemeral", hst.FilesystemConfigJSON{
FilesystemConfig: &hst.FSEphemeral{
Target: m("/run/user/65534"),
Write: true,
Size: 1 << 10,
Perm: 0700,
},
}, nil,
`{"type":"ephemeral","dst":"/run/user/65534","write":true,"size":1024,"perm":448}`,
`{"fs":{"type":"ephemeral","dst":"/run/user/65534","write":true,"size":1024,"perm":448},"magic":3236757504}`},
{"overlay", hst.FilesystemConfigJSON{
FilesystemConfig: &hst.FSOverlay{
Target: m("/nix/store"),
Lower: ms("/mnt-root/nix/.ro-store"),
Upper: m("/mnt-root/nix/.rw-store/upper"),
Work: m("/mnt-root/nix/.rw-store/work"),
},
}, nil,
`{"type":"overlay","dst":"/nix/store","lower":["/mnt-root/nix/.ro-store"],"upper":"/mnt-root/nix/.rw-store/upper","work":"/mnt-root/nix/.rw-store/work"}`,
`{"fs":{"type":"overlay","dst":"/nix/store","lower":["/mnt-root/nix/.ro-store"],"upper":"/mnt-root/nix/.rw-store/upper","work":"/mnt-root/nix/.rw-store/work"},"magic":3236757504}`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("marshal", func(t *testing.T) {
wantErr := tc.wantErr
if errors.As(wantErr, new(hst.FSTypeError)) {
// for unsupported implementation tc
wantErr = hst.FSImplError{Value: stubFS{"cat"}}
}
{
d, err := json.Marshal(&tc.want)
if !errors.Is(err, wantErr) {
t.Errorf("Marshal: error = %v, want %v", err, wantErr)
}
if wantErr != nil {
goto checkSMarshal
}
if string(d) != tc.data {
t.Errorf("Marshal:\n%s\nwant:\n%s", string(d), tc.data)
}
}
checkSMarshal:
{
d, err := json.Marshal(&sCheck{tc.want, syscall.MS_MGC_VAL})
if !errors.Is(err, wantErr) {
t.Errorf("Marshal: error = %v, want %v", err, wantErr)
}
if wantErr != nil {
return
}
if string(d) != tc.sData {
t.Errorf("Marshal:\n%s\nwant:\n%s", string(d), tc.sData)
}
}
})
t.Run("unmarshal", func(t *testing.T) {
if tc.data == "\x00" && tc.sData == "\x00" {
if errors.As(tc.wantErr, new(hst.FSImplError)) {
// this error is only returned on marshal
return
}
}
{
var got hst.FilesystemConfigJSON
err := json.Unmarshal([]byte(tc.data), &got)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Unmarshal: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
goto checkSUnmarshal
}
if !reflect.DeepEqual(&tc.want, &got) {
t.Errorf("Unmarshal: %#v, want %#v", &tc.want, &got)
}
}
checkSUnmarshal:
{
var got sCheck
err := json.Unmarshal([]byte(tc.sData), &got)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Unmarshal: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
return
}
want := sCheck{tc.want, syscall.MS_MGC_VAL}
if !reflect.DeepEqual(&got, &want) {
t.Errorf("Unmarshal: %#v, want %#v", &got, &want)
}
}
})
})
}
t.Run("valid", func(t *testing.T) {
if got := (*hst.FilesystemConfigJSON).Valid(nil); got {
t.Errorf("Valid: %v, want false", got)
}
if got := new(hst.FilesystemConfigJSON).Valid(); got {
t.Errorf("Valid: %v, want false", got)
}
if got := (&hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: m("/etc")}}).Valid(); !got {
t.Errorf("Valid: %v, want true", got)
}
})
t.Run("passthrough", func(t *testing.T) {
if err := new(hst.FilesystemConfigJSON).UnmarshalJSON(make([]byte, 0)); err == nil {
t.Errorf("UnmarshalJSON: error = %v", err)
}
})
}
func TestFSErrors(t *testing.T) {
t.Run("type", func(t *testing.T) {
want := `invalid filesystem type "cat"`
if got := hst.FSTypeError("cat").Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
t.Run("impl", func(t *testing.T) {
testCases := []struct {
name string
val hst.FilesystemConfig
want string
}{
{"nil", nil, "implementation nil not supported"},
{"stub", stubFS{"cat"}, "implementation stubFS not supported"},
{"*stub", &stubFS{"cat"}, "implementation *stubFS not supported"},
{"(*stub)(nil)", (*stubFS)(nil), "implementation *stubFS not supported"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := hst.FSImplError{Value: tc.val}
if got := err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want)
}
})
}
})
}
type stubFS struct {
typeName string
}
func (s stubFS) Valid() bool { return false }
func (s stubFS) Path() *container.Absolute { panic("unreachable") }
func (s stubFS) Host() []*container.Absolute { panic("unreachable") }
func (s stubFS) Apply(*container.Ops) { panic("unreachable") }
func (s stubFS) String() string { return "<invalid " + s.typeName + ">" }
type sCheck struct {
FS hst.FilesystemConfigJSON `json:"fs"`
Magic int `json:"magic"`
}
type fsTestCase struct {
name string
fs hst.FilesystemConfig
valid bool
ops container.Ops
path *container.Absolute
host []*container.Absolute
str string
}
func checkFs(t *testing.T, testCases []fsTestCase) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("valid", func(t *testing.T) {
if got := tc.fs.Valid(); got != tc.valid {
t.Errorf("Valid: %v, want %v", got, tc.valid)
}
})
t.Run("ops", func(t *testing.T) {
ops := new(container.Ops)
tc.fs.Apply(ops)
if !reflect.DeepEqual(ops, &tc.ops) {
gotString := new(strings.Builder)
for _, op := range *ops {
gotString.WriteString("\n" + op.String())
}
wantString := new(strings.Builder)
for _, op := range tc.ops {
wantString.WriteString("\n" + op.String())
}
t.Errorf("Apply: %s, want %s", gotString, wantString)
}
})
t.Run("path", func(t *testing.T) {
if got := tc.fs.Path(); !reflect.DeepEqual(got, tc.path) {
t.Errorf("Target: %q, want %q", got, tc.path)
}
})
t.Run("host", func(t *testing.T) {
if got := tc.fs.Host(); !reflect.DeepEqual(got, tc.host) {
t.Errorf("Host: %q, want %q", got, tc.host)
}
})
t.Run("string", func(t *testing.T) {
if tc.str == "\x00" {
return
}
if got := tc.fs.String(); got != tc.str {
t.Errorf("String: %q, want %q", got, tc.str)
}
})
})
}
}
func m(pathname string) *container.Absolute { return container.MustAbs(pathname) }
func ms(pathnames ...string) []*container.Absolute {
as := make([]*container.Absolute, len(pathnames))
for i, pathname := range pathnames {
as[i] = container.MustAbs(pathname)
}
return as
}

View File

@ -1,102 +0,0 @@
package hst
import (
"encoding/gob"
"strings"
"hakurei.app/container"
)
func init() { gob.Register(new(FSBind)) }
// FilesystemBind is the [FilesystemConfig.Type] name of a bind mount point.
const FilesystemBind = "bind"
// FSBind represents a host to container bind mount.
type FSBind struct {
// mount point in container, same as src if empty
Target *container.Absolute `json:"dst,omitempty"`
// host filesystem path to make available to the container
Source *container.Absolute `json:"src"`
// do not mount filesystem read-only
Write bool `json:"write,omitempty"`
// do not disable device files, implies Write
Device bool `json:"dev,omitempty"`
// skip this mount point if the host path does not exist
Optional bool `json:"optional,omitempty"`
}
func (b *FSBind) Valid() bool { return b != nil && b.Source != nil }
func (b *FSBind) Path() *container.Absolute {
if !b.Valid() {
return nil
}
if b.Target == nil {
return b.Source
}
return b.Target
}
func (b *FSBind) Host() []*container.Absolute {
if !b.Valid() {
return nil
}
return []*container.Absolute{b.Source}
}
func (b *FSBind) Apply(ops *container.Ops) {
if !b.Valid() {
return
}
target := b.Target
if target == nil {
target = b.Source
}
var flags int
if b.Write {
flags |= container.BindWritable
}
if b.Device {
flags |= container.BindDevice | container.BindWritable
}
if b.Optional {
flags |= container.BindOptional
}
ops.Bind(b.Source, target, flags)
}
func (b *FSBind) String() string {
g := 4
if !b.Valid() {
return "<invalid>"
}
g += len(b.Source.String())
if b.Target != nil {
g += len(b.Target.String())
}
expr := new(strings.Builder)
expr.Grow(g)
if b.Device {
expr.WriteString("d")
} else if b.Write {
expr.WriteString("w")
}
if !b.Optional {
expr.WriteString("*")
} else {
expr.WriteString("+")
}
expr.WriteString(b.Source.String())
if b.Target != nil {
expr.WriteString(":" + b.Target.String())
}
return expr.String()
}

View File

@ -1,66 +0,0 @@
package hst_test
import (
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestFSBind(t *testing.T) {
checkFs(t, []fsTestCase{
{"nil", (*hst.FSBind)(nil), false, nil, nil, nil, "<invalid>"},
{"full", &hst.FSBind{
Target: m("/dev"),
Source: m("/mnt/dev"),
Optional: true,
Device: true,
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/dev"),
Target: m("/dev"),
Flags: container.BindWritable | container.BindDevice | container.BindOptional,
}}, m("/dev"), ms("/mnt/dev"),
"d+/mnt/dev:/dev"},
{"full write dev", &hst.FSBind{
Target: m("/dev"),
Source: m("/mnt/dev"),
Write: true,
Device: true,
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/dev"),
Target: m("/dev"),
Flags: container.BindWritable | container.BindDevice,
}}, m("/dev"), ms("/mnt/dev"),
"d*/mnt/dev:/dev"},
{"full write", &hst.FSBind{
Target: m("/tmp"),
Source: m("/mnt/tmp"),
Write: true,
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/tmp"),
Target: m("/tmp"),
Flags: container.BindWritable,
}}, m("/tmp"), ms("/mnt/tmp"),
"w*/mnt/tmp:/tmp"},
{"full no flags", &hst.FSBind{
Target: m("/etc"),
Source: m("/mnt/etc"),
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/etc"),
Target: m("/etc"),
}}, m("/etc"), ms("/mnt/etc"),
"*/mnt/etc:/etc"},
{"nil dst", &hst.FSBind{
Source: m("/"),
}, true, container.Ops{&container.BindMountOp{
Source: m("/"),
Target: m("/"),
}}, m("/"), ms("/"),
"*/"},
})
}

View File

@ -1,83 +0,0 @@
package hst
import (
"encoding/gob"
"os"
"strings"
"hakurei.app/container"
)
func init() { gob.Register(new(FSEphemeral)) }
// FilesystemEphemeral is the [FilesystemConfig.Type] name of a mount point with ephemeral state.
const FilesystemEphemeral = "ephemeral"
// FSEphemeral represents an ephemeral container mount point.
type FSEphemeral struct {
// mount point in container
Target *container.Absolute `json:"dst,omitempty"`
// do not mount filesystem read-only
Write bool `json:"write,omitempty"`
// upper limit on the size of the filesystem
Size int `json:"size,omitempty"`
// initial permission bits of the new filesystem
Perm os.FileMode `json:"perm,omitempty"`
}
func (e *FSEphemeral) Valid() bool { return e != nil && e.Target != nil }
func (e *FSEphemeral) Path() *container.Absolute {
if !e.Valid() {
return nil
}
return e.Target
}
func (e *FSEphemeral) Host() []*container.Absolute { return nil }
const fsEphemeralDefaultPerm = os.FileMode(0755)
func (e *FSEphemeral) Apply(ops *container.Ops) {
if !e.Valid() {
return
}
size := e.Size
if size < 0 {
size = 0
}
perm := e.Perm
if perm == 0 {
perm = fsEphemeralDefaultPerm
}
if e.Write {
ops.Tmpfs(e.Target, size, perm)
} else {
ops.Readonly(e.Target, perm)
}
}
func (e *FSEphemeral) String() string {
if !e.Valid() {
return "<invalid>"
}
expr := new(strings.Builder)
expr.Grow(15 + len(FilesystemEphemeral) + len(e.Target.String()))
if e.Write {
expr.WriteString("w")
}
expr.WriteString("+" + FilesystemEphemeral + "(")
if e.Perm != 0 {
expr.WriteString(e.Perm.String())
} else {
expr.WriteString(fsEphemeralDefaultPerm.String())
}
expr.WriteString("):" + e.Target.String())
return expr.String()
}

View File

@ -1,50 +0,0 @@
package hst_test
import (
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestFSEphemeral(t *testing.T) {
checkFs(t, []fsTestCase{
{"nil", (*hst.FSEphemeral)(nil), false, nil, nil, nil, "<invalid>"},
{"full", &hst.FSEphemeral{
Target: m("/run/user/65534"),
Write: true,
Size: 1 << 10,
Perm: 0700,
}, true, container.Ops{&container.MountTmpfsOp{
FSName: "ephemeral",
Path: m("/run/user/65534"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0700,
}}, m("/run/user/65534"), nil,
"w+ephemeral(-rwx------):/run/user/65534"},
{"cover ro", &hst.FSEphemeral{Target: m("/run/nscd")}, true,
container.Ops{&container.MountTmpfsOp{
FSName: "readonly",
Path: m("/run/nscd"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
Perm: 0755,
}}, m("/run/nscd"), nil,
"+ephemeral(-rwxr-xr-x):/run/nscd"},
{"negative size", &hst.FSEphemeral{
Target: hst.AbsTmp,
Write: true,
Size: -1,
}, true, container.Ops{&container.MountTmpfsOp{
FSName: "ephemeral",
Path: hst.AbsTmp,
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Perm: 0755,
}}, hst.AbsTmp, nil,
"w+ephemeral(-rwxr-xr-x):/.hakurei"},
})
}

View File

@ -1,98 +0,0 @@
package hst
import (
"encoding/gob"
"strings"
"hakurei.app/container"
)
func init() { gob.Register(new(FSOverlay)) }
// FilesystemOverlay is the [FilesystemConfig.Type] name of an overlay mount point.
const FilesystemOverlay = "overlay"
// FSOverlay represents an overlay mount point.
type FSOverlay struct {
// mount point in container
Target *container.Absolute `json:"dst"`
// any filesystem, does not need to be on a writable filesystem, must not be nil
Lower []*container.Absolute `json:"lower"`
// the upperdir is normally on a writable filesystem, leave as nil to mount Lower readonly
Upper *container.Absolute `json:"upper,omitempty"`
// the workdir needs to be an empty directory on the same filesystem as Upper, must not be nil if Upper is populated
Work *container.Absolute `json:"work,omitempty"`
}
func (o *FSOverlay) Valid() bool {
if o == nil || o.Target == nil {
return false
}
for _, a := range o.Lower {
if a == nil {
return false
}
}
if o.Upper != nil { // rw
return o.Work != nil && len(o.Lower) > 0
} else { // ro
return len(o.Lower) >= 2
}
}
func (o *FSOverlay) Path() *container.Absolute {
if !o.Valid() {
return nil
}
return o.Target
}
func (o *FSOverlay) Host() []*container.Absolute {
if !o.Valid() {
return nil
}
p := make([]*container.Absolute, 0, 2+len(o.Lower))
if o.Upper != nil && o.Work != nil {
p = append(p, o.Upper, o.Work)
}
p = append(p, o.Lower...)
return p
}
func (o *FSOverlay) Apply(op *container.Ops) {
if !o.Valid() {
return
}
if o.Upper != nil && o.Work != nil { // rw
op.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
} else { // ro
op.OverlayReadonly(o.Target, o.Lower...)
}
}
func (o *FSOverlay) String() string {
if !o.Valid() {
return "<invalid>"
}
lower := make([]string, len(o.Lower))
for i, a := range o.Lower {
lower[i] = container.EscapeOverlayDataSegment(a.String())
}
if o.Upper != nil && o.Work != nil {
return "w*" + strings.Join(append([]string{
container.EscapeOverlayDataSegment(o.Target.String()),
container.EscapeOverlayDataSegment(o.Upper.String()),
container.EscapeOverlayDataSegment(o.Work.String())},
lower...), container.SpecialOverlayPath)
} else {
return "*" + strings.Join(append([]string{
container.EscapeOverlayDataSegment(o.Target.String())},
lower...), container.SpecialOverlayPath)
}
}

View File

@ -1,50 +0,0 @@
package hst_test
import (
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestFSOverlay(t *testing.T) {
checkFs(t, []fsTestCase{
{"nil", (*hst.FSOverlay)(nil), false, nil, nil, nil, "<invalid>"},
{"nil lower", &hst.FSOverlay{Target: m("/etc"), Lower: []*container.Absolute{nil}}, false, nil, nil, nil, "<invalid>"},
{"zero lower", &hst.FSOverlay{Target: m("/etc"), Upper: m("/"), Work: m("/")}, false, nil, nil, nil, "<invalid>"},
{"zero lower ro", &hst.FSOverlay{Target: m("/etc")}, false, nil, nil, nil, "<invalid>"},
{"short lower", &hst.FSOverlay{Target: m("/etc"), Lower: ms("/etc")}, false, nil, nil, nil, "<invalid>"},
{"full", &hst.FSOverlay{
Target: m("/nix/store"),
Lower: ms("/mnt-root/nix/.ro-store"),
Upper: m("/mnt-root/nix/.rw-store/upper"),
Work: m("/mnt-root/nix/.rw-store/work"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/nix/store"),
Lower: ms("/mnt-root/nix/.ro-store"),
Upper: m("/mnt-root/nix/.rw-store/upper"),
Work: m("/mnt-root/nix/.rw-store/work"),
}}, m("/nix/store"), ms("/mnt-root/nix/.rw-store/upper", "/mnt-root/nix/.rw-store/work", "/mnt-root/nix/.ro-store"),
"w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store"},
{"ro", &hst.FSOverlay{
Target: m("/mnt/src"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/mnt/src"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
"*/mnt/src:/tmp/.src0:/tmp/.src1"},
{"ro work", &hst.FSOverlay{
Target: m("/mnt/src"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
Work: m("/tmp"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/mnt/src"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
"*/mnt/src:/tmp/.src0:/tmp/.src1"},
})
}

View File

@ -1,120 +0,0 @@
// Package hst exports stable shared types for interacting with hakurei.
package hst
import (
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
// Paths contains environment-dependent paths used by hakurei.
type Paths struct {
// temporary directory returned by [os.TempDir] (usually `/tmp`)
TempDir *container.Absolute `json:"temp_dir"`
// path to shared directory (usually `/tmp/hakurei.%d`)
SharePath *container.Absolute `json:"share_path"`
// XDG_RUNTIME_DIR value (usually `/run/user/%d`)
RuntimePath *container.Absolute `json:"runtime_path"`
// application runtime directory (usually `/run/user/%d/hakurei`)
RunDirPath *container.Absolute `json:"run_dir_path"`
}
type Info struct {
User int `json:"user"`
Paths
}
// Template returns a fully populated instance of Config.
func Template() *Config {
return &Config{
ID: "org.chromium.Chromium",
Path: container.AbsFHSRun.Append("current-system/sw/bin/chromium"),
Args: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland",
},
Enablements: NewEnablements(system.EWayland | system.EDBus | system.EPulse),
SessionBus: &dbus.Config{
See: nil,
Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver",
"org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"},
Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"},
Call: map[string]string{"org.freedesktop.portal.*": "*"},
Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
Log: false,
Filter: true,
},
SystemBus: &dbus.Config{
See: nil,
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Own: nil,
Call: nil,
Broadcast: nil,
Log: false,
Filter: true,
},
DirectWayland: false,
Username: "chronos",
Shell: container.AbsFHSRun.Append("current-system/sw/bin/zsh"),
Data: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"),
Dir: container.MustAbs("/data/data/org.chromium.Chromium"),
ExtraPerms: []*ExtraPermConfig{
{Path: container.AbsFHSVarLib.Append("hakurei/u0"), Ensure: true, Execute: true},
{Path: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"), Read: true, Write: true, Execute: true},
},
Identity: 9,
Groups: []string{"video", "dialout", "plugdev"},
Container: &ContainerConfig{
Hostname: "localhost",
Devel: true,
Userns: true,
Net: true,
Device: true,
WaitDelay: -1,
SeccompFlags: seccomp.AllowMultiarch,
SeccompPresets: seccomp.PresetExt,
SeccompCompat: true,
Tty: true,
Multiarch: true,
MapRealUID: true,
// example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
},
Filesystem: []FilesystemConfigJSON{
{&FSEphemeral{Target: container.AbsFHSTmp, Write: true, Perm: 0755}},
{&FSOverlay{
Target: container.MustAbs("/nix/store"),
Lower: []*container.Absolute{container.MustAbs("/mnt-root/nix/.ro-store")},
Upper: container.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: container.MustAbs("/mnt-root/nix/.rw-store/work"),
}},
{&FSBind{Source: container.MustAbs("/nix/store")}},
{&FSBind{Source: container.AbsFHSRun.Append("current-system")}},
{&FSBind{Source: container.AbsFHSRun.Append("opengl-driver")}},
{&FSBind{Source: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"),
Target: container.MustAbs("/data/data/org.chromium.Chromium"), Write: true}},
{&FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}},
},
Link: []LinkConfig{{container.AbsFHSRunUser.Append("65534"), container.FHSRunUser + "150"}},
AutoRoot: container.AbsFHSVarLib.Append("hakurei/base/org.debian"),
RootFlags: container.BindWritable,
Etc: container.AbsFHSEtc,
AutoEtc: true,
},
}
}

5
hst/info.go Normal file
View File

@ -0,0 +1,5 @@
package hst
type Info struct {
User int `json:"user"`
}

11
hst/paths.go Normal file
View File

@ -0,0 +1,11 @@
package hst
// Paths contains environment-dependent paths used by hakurei.
type Paths struct {
// path to shared directory (usually `/tmp/hakurei.%d`)
SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value (usually `/run/user/%d`)
RuntimePath string `json:"runtime_path"`
// application runtime directory (usually `/run/user/%d/hakurei`)
RunDirPath string `json:"run_dir_path"`
}

92
hst/template.go Normal file
View File

@ -0,0 +1,92 @@
package hst
import (
"hakurei.app/container/seccomp"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
// Template returns a fully populated instance of Config.
func Template() *Config {
return &Config{
ID: "org.chromium.Chromium",
Path: "/run/current-system/sw/bin/chromium",
Args: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland",
},
Enablements: system.EWayland | system.EDBus | system.EPulse,
SessionBus: &dbus.Config{
See: nil,
Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver",
"org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"},
Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"},
Call: map[string]string{"org.freedesktop.portal.*": "*"},
Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
Log: false,
Filter: true,
},
SystemBus: &dbus.Config{
See: nil,
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Own: nil,
Call: nil,
Broadcast: nil,
Log: false,
Filter: true,
},
DirectWayland: false,
Username: "chronos",
Shell: "/run/current-system/sw/bin/zsh",
Data: "/var/lib/hakurei/u0/org.chromium.Chromium",
Dir: "/data/data/org.chromium.Chromium",
ExtraPerms: []*ExtraPermConfig{
{Path: "/var/lib/hakurei/u0", Ensure: true, Execute: true},
{Path: "/var/lib/hakurei/u0/org.chromium.Chromium", Read: true, Write: true, Execute: true},
},
Identity: 9,
Groups: []string{"video", "dialout", "plugdev"},
Container: &ContainerConfig{
Hostname: "localhost",
Devel: true,
Userns: true,
Net: true,
Device: true,
SeccompFlags: seccomp.AllowMultiarch,
SeccompPresets: seccomp.PresetExt,
Tty: true,
Multiarch: true,
MapRealUID: true,
// example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
},
Filesystem: []*FilesystemConfig{
{Src: "/nix/store"},
{Src: "/run/current-system"},
{Src: "/run/opengl-driver"},
{Src: "/var/db/nix-channels"},
{Src: "/var/lib/hakurei/u0/org.chromium.Chromium",
Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
{Src: "/dev/dri", Device: true},
},
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
Etc: "/etc",
AutoEtc: true,
Cover: []string{"/var/run/nscd"},
},
}
}

View File

@ -18,11 +18,7 @@ func TestTemplate(t *testing.T) {
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
], ],
"enablements": { "enablements": 13,
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": { "session_bus": {
"see": null, "see": null,
"talk": [ "talk": [
@ -84,10 +80,8 @@ func TestTemplate(t *testing.T) {
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
@ -102,55 +96,39 @@ func TestTemplate(t *testing.T) {
"device": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"type": "ephemeral",
"dst": "/tmp/",
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store" "src": "/nix/store"
}, },
{ {
"type": "bind",
"src": "/run/current-system" "src": "/run/current-system"
}, },
{ {
"type": "bind",
"src": "/run/opengl-driver" "src": "/run/opengl-driver"
}, },
{ {
"type": "bind", "src": "/var/db/nix-channels"
"dst": "/data/data/org.chromium.Chromium", },
"src": "/var/lib/hakurei/u0/org.chromium.Chromium", {
"write": true "dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true,
"require": true
}, },
{ {
"type": "bind",
"src": "/dev/dri", "src": "/dev/dri",
"dev": true, "dev": true
"optional": true
} }
], ],
"symlink": [ "symlink": [
{ [
"target": "/run/user/65534", "/run/user/65534",
"linkname": "/run/user/150" "/run/user/150"
} ]
], ],
"auto_root": "/var/lib/hakurei/base/org.debian", "etc": "/etc",
"root_flags": 2, "auto_etc": true,
"etc": "/etc/", "cover": [
"auto_etc": true "/var/run/nscd"
]
} }
}` }`

View File

@ -7,6 +7,7 @@ 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"
) )
@ -58,6 +59,10 @@ 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,7 +11,6 @@ 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"
) )
@ -37,7 +36,6 @@ 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

@ -1,8 +1,6 @@
package app_test package app_test
import ( import (
"syscall"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
@ -12,35 +10,23 @@ import (
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
func m(pathname string) *container.Absolute { return container.MustAbs(pathname) }
func f(c hst.FilesystemConfig) hst.FilesystemConfigJSON {
return hst.FilesystemConfigJSON{FilesystemConfig: c}
}
var testCasesNixos = []sealTestCase{ 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: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"), Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Enablements: hst.NewEnablements(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.FilesystemConfigJSON{ Filesystem: []*hst.FilesystemConfig{
f(&hst.FSBind{Source: m("/bin")}), {Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
f(&hst.FSBind{Source: m("/usr/bin/")}), {Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
f(&hst.FSBind{Source: m("/nix/store")}), {Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
f(&hst.FSBind{Source: m("/run/current-system")}), {Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
f(&hst.FSBind{Source: m("/sys/block"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/bus"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/class"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/dev"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/devices"), Optional: true}),
f(&hst.FSBind{Source: m("/run/opengl-driver")}),
f(&hst.FSBind{Source: m("/dev/dri"), Device: true, Optional: true}),
}, },
Cover: []string{"/var/run/nscd"},
}, },
SystemBus: &dbus.Config{ SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"}, Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
@ -63,7 +49,7 @@ var testCasesNixos = []sealTestCase{
DirectWayland: true, DirectWayland: true,
Username: "u0_a1", Username: "u0_a1",
Data: m("/var/lib/persist/module/hakurei/0/1"), Data: "/var/lib/persist/module/hakurei/0/1",
Identity: 1, Groups: []string{}, Identity: 1, Groups: []string{},
}, },
state.ID{ state.ID{
@ -111,8 +97,8 @@ var testCasesNixos = []sealTestCase{
&container.Params{ &container.Params{
Uid: 1971, Uid: 1971,
Gid: 100, Gid: 100,
Dir: m("/var/lib/persist/module/hakurei/0/1"), Dir: "/var/lib/persist/module/hakurei/0/1",
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"), Path: "/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",
@ -129,37 +115,35 @@ var testCasesNixos = []sealTestCase{
"XDG_SESSION_TYPE=tty", "XDG_SESSION_TYPE=tty",
}, },
Ops: new(container.Ops). Ops: new(container.Ops).
Proc(m("/proc/")). Proc("/proc").
Tmpfs(hst.AbsTmp, 4096, 0755). Tmpfs(hst.Tmp, 4096, 0755).
DevWritable(m("/dev/"), true). Dev("/dev").Mqueue("/dev/mqueue").
Bind(m("/bin"), m("/bin"), 0). Bind("/bin", "/bin", 0).
Bind(m("/usr/bin/"), m("/usr/bin/"), 0). Bind("/usr/bin", "/usr/bin", 0).
Bind(m("/nix/store"), m("/nix/store"), 0). Bind("/nix/store", "/nix/store", 0).
Bind(m("/run/current-system"), m("/run/current-system"), 0). Bind("/run/current-system", "/run/current-system", 0).
Bind(m("/sys/block"), m("/sys/block"), container.BindOptional). Bind("/sys/block", "/sys/block", container.BindOptional).
Bind(m("/sys/bus"), m("/sys/bus"), container.BindOptional). Bind("/sys/bus", "/sys/bus", container.BindOptional).
Bind(m("/sys/class"), m("/sys/class"), container.BindOptional). Bind("/sys/class", "/sys/class", container.BindOptional).
Bind(m("/sys/dev"), m("/sys/dev"), container.BindOptional). Bind("/sys/dev", "/sys/dev", container.BindOptional).
Bind(m("/sys/devices"), m("/sys/devices"), container.BindOptional). Bind("/sys/devices", "/sys/devices", container.BindOptional).
Bind(m("/run/opengl-driver"), m("/run/opengl-driver"), 0). Bind("/run/opengl-driver", "/run/opengl-driver", 0).
Bind(m("/dev/dri"), m("/dev/dri"), container.BindDevice|container.BindWritable|container.BindOptional). Bind("/dev/dri", "/dev/dri", container.BindDevice|container.BindWritable|container.BindOptional).
Etc(m("/etc/"), "8e2c76b066dabe574cf073bdb46eb5c1"). Etc("/etc", "8e2c76b066dabe574cf073bdb46eb5c1").
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). Tmpfs("/var/run/nscd", 8192, 0755),
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,
}, },
}, },
} }

View File

@ -2,7 +2,6 @@ package app_test
import ( import (
"os" "os"
"syscall"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
@ -16,7 +15,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: m("/home/chronos")}, &hst.Config{Username: "chronos", Data: "/home/chronos"},
state.ID{ state.ID{
0x4a, 0x45, 0x0b, 0x65, 0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15, 0x96, 0xd7, 0xbc, 0x15,
@ -30,8 +29,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: m("/home/chronos"), Dir: "/home/chronos",
Path: m("/run/current-system/sw/bin/zsh"), Path: "/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,27 +42,35 @@ var testCasesPd = []sealTestCase{
"XDG_SESSION_TYPE=tty", "XDG_SESSION_TYPE=tty",
}, },
Ops: new(container.Ops). Ops: new(container.Ops).
Root(m("/"), "4a450b6596d7bc15bd01780eb9a607ac", container.BindWritable). Proc("/proc").
Proc(m("/proc/")). Tmpfs(hst.Tmp, 4096, 0755).
Tmpfs(hst.AbsTmp, 4096, 0755). Dev("/dev").Mqueue("/dev/mqueue").
DevWritable(m("/dev/"), true). Bind("/bin", "/bin", container.BindWritable).
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional). Bind("/boot", "/boot", container.BindWritable).
Readonly(m("/var/run/nscd"), 0755). Bind("/home", "/home", container.BindWritable).
Tmpfs(m("/run/user/1971"), 8192, 0755). Bind("/lib", "/lib", container.BindWritable).
Tmpfs(m("/run/dbus"), 8192, 0755). Bind("/lib64", "/lib64", container.BindWritable).
Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac"). Bind("/nix", "/nix", container.BindWritable).
Remount(m("/dev/"), syscall.MS_RDONLY). Bind("/root", "/root", container.BindWritable).
Tmpfs(m("/run/user/"), 4096, 0755). Bind("/run", "/run", container.BindWritable).
Bind(m("/tmp/hakurei.1971/runtime/0"), m("/run/user/65534"), container.BindWritable). Bind("/srv", "/srv", container.BindWritable).
Bind(m("/tmp/hakurei.1971/tmpdir/0"), m("/tmp/"), container.BindWritable). Bind("/sys", "/sys", container.BindWritable).
Bind(m("/home/chronos"), m("/home/chronos"), container.BindWritable). Bind("/usr", "/usr", container.BindWritable).
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). Bind("/var", "/var", container.BindWritable).
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")). Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional).
Remount(m("/"), syscall.MS_RDONLY), Tmpfs("/run/user/1971", 8192, 0755).
Tmpfs("/run/dbus", 8192, 0755).
Etc("/etc", "4a450b6596d7bc15bd01780eb9a607ac").
Tmpfs("/run/user", 4096, 0755).
Bind("/tmp/hakurei.1971/runtime/0", "/run/user/65534", container.BindWritable).
Bind("/tmp/hakurei.1971/tmpdir/0", "/tmp", container.BindWritable).
Bind("/home/chronos", "/home/chronos", container.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("hakurei:x:65534:\n")).
Tmpfs("/var/run/nscd", 8192, 0755),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
RetainSession: true, RetainSession: true,
ForwardCancel: true,
}, },
}, },
{ {
@ -74,7 +81,7 @@ var testCasesPd = []sealTestCase{
Identity: 9, Identity: 9,
Groups: []string{"video"}, Groups: []string{"video"},
Username: "chronos", Username: "chronos",
Data: m("/home/chronos"), Data: "/home/chronos",
SessionBus: &dbus.Config{ SessionBus: &dbus.Config{
Talk: []string{ Talk: []string{
"org.freedesktop.Notifications", "org.freedesktop.Notifications",
@ -106,7 +113,7 @@ var testCasesPd = []sealTestCase{
}, },
Filter: true, Filter: true,
}, },
Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse), Enablements: system.EWayland | system.EDBus | system.EPulse,
}, },
state.ID{ state.ID{
0xeb, 0xf0, 0x83, 0xd1, 0xeb, 0xf0, 0x83, 0xd1,
@ -160,8 +167,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: m("/home/chronos"), Dir: "/home/chronos",
Path: m("/run/current-system/sw/bin/zsh"), Path: "/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,33 +185,41 @@ var testCasesPd = []sealTestCase{
"XDG_SESSION_TYPE=tty", "XDG_SESSION_TYPE=tty",
}, },
Ops: new(container.Ops). Ops: new(container.Ops).
Root(m("/"), "ebf083d1b175911782d413369b64ce7c", container.BindWritable). Proc("/proc").
Proc(m("/proc/")). Tmpfs(hst.Tmp, 4096, 0755).
Tmpfs(hst.AbsTmp, 4096, 0755). Dev("/dev").Mqueue("/dev/mqueue").
DevWritable(m("/dev/"), true). Bind("/bin", "/bin", container.BindWritable).
Bind(m("/dev/dri"), m("/dev/dri"), container.BindWritable|container.BindDevice|container.BindOptional). Bind("/boot", "/boot", container.BindWritable).
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional). Bind("/home", "/home", container.BindWritable).
Readonly(m("/var/run/nscd"), 0755). Bind("/lib", "/lib", container.BindWritable).
Tmpfs(m("/run/user/1971"), 8192, 0755). Bind("/lib64", "/lib64", container.BindWritable).
Tmpfs(m("/run/dbus"), 8192, 0755). Bind("/nix", "/nix", container.BindWritable).
Etc(m("/etc/"), "ebf083d1b175911782d413369b64ce7c"). Bind("/root", "/root", container.BindWritable).
Remount(m("/dev/"), syscall.MS_RDONLY). Bind("/run", "/run", container.BindWritable).
Tmpfs(m("/run/user/"), 4096, 0755). Bind("/srv", "/srv", container.BindWritable).
Bind(m("/tmp/hakurei.1971/runtime/9"), m("/run/user/65534"), container.BindWritable). Bind("/sys", "/sys", container.BindWritable).
Bind(m("/tmp/hakurei.1971/tmpdir/9"), m("/tmp/"), container.BindWritable). Bind("/usr", "/usr", container.BindWritable).
Bind(m("/home/chronos"), m("/home/chronos"), container.BindWritable). Bind("/var", "/var", container.BindWritable).
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). Bind("/dev/dri", "/dev/dri", container.BindWritable|container.BindDevice|container.BindOptional).
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")). Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional).
Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0). Tmpfs("/run/user/1971", 8192, 0755).
Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0). Tmpfs("/run/dbus", 8192, 0755).
Place(m(hst.Tmp+"/pulse-cookie"), nil). Etc("/etc", "ebf083d1b175911782d413369b64ce7c").
Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0). Tmpfs("/run/user", 4096, 0755).
Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0). Bind("/tmp/hakurei.1971/runtime/9", "/run/user/65534", container.BindWritable).
Remount(m("/"), syscall.MS_RDONLY), Bind("/tmp/hakurei.1971/tmpdir/9", "/tmp", container.BindWritable).
Bind("/home/chronos", "/home/chronos", container.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("hakurei:x:65534:\n")).
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0", 0).
Bind("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
Place(hst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
RetainSession: true, RetainSession: true,
ForwardCancel: 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: m("/tmp/hakurei.1971"), SharePath: "/tmp/hakurei.1971",
RuntimePath: m("/run/user/1971"), RuntimePath: "/run/user/1971",
RunDirPath: m("/run/user/1971/hakurei"), RunDirPath: "/run/user/1971/hakurei",
} }
} }

View File

@ -11,7 +11,6 @@ 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"
) )
@ -22,9 +21,9 @@ const preallocateOpsCount = 1 << 5
// newContainer initialises [container.Params] via [hst.ContainerConfig]. // newContainer initialises [container.Params] via [hst.ContainerConfig].
// Note that remaining container setup must be queued by the caller. // Note that remaining container setup must be queued by the caller.
func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid *int) (*container.Params, map[string]string, error) { func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*container.Params, map[string]string, error) {
if s == nil { if s == nil {
return nil, nil, hlog.WrapErr(syscall.EBADE, "invalid container configuration") return nil, nil, syscall.EBADE
} }
params := &container.Params{ params := &container.Params{
@ -34,14 +33,10 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
RetainSession: s.Tty, RetainSession: s.Tty,
HostNet: s.Net, HostNet: s.Net,
ScopeAbstract: s.ScopeAbstract, ScopeAbstract: s.ScopeAbstract,
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
// this behaviour is implemented in the shim
ForwardCancel: s.WaitDelay >= 0,
} }
{ {
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link)) ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link)+len(s.Cover))
params.Ops = &ops params.Ops = &ops
} }
@ -74,18 +69,14 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
*gid = container.OverflowGid() *gid = container.OverflowGid()
} }
if s.AutoRoot != nil {
params.Root(s.AutoRoot, prefix, s.RootFlags)
}
params. params.
Proc(container.AbsFHSProc). Proc("/proc").
Tmpfs(hst.AbsTmp, 1<<12, 0755) Tmpfs(hst.Tmp, 1<<12, 0755)
if !s.Device { if !s.Device {
params.DevWritable(container.AbsFHSDev, true) params.Dev("/dev").Mqueue("/dev/mqueue")
} else { } else {
params.Bind(container.AbsFHSDev, container.AbsFHSDev, container.BindWritable|container.BindDevice) params.Bind("/dev", "/dev", 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;
@ -94,7 +85,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.String(), sc.SharePath.String()) hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
_, 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
@ -109,7 +100,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
if path.IsAbs(pair[1]) { if path.IsAbs(pair[1]) {
// get parent dir of socket // get parent dir of socket
dir := path.Dir(pair[1]) dir := path.Dir(pair[1])
if dir == "." || dir == container.FHSRoot { if dir == "." || dir == "/" {
os.Printf("dbus socket %q is in an unusual location", pair[1]) os.Printf("dbus socket %q is in an unusual location", pair[1])
} }
hidePaths = append(hidePaths, dir) hidePaths = append(hidePaths, dir)
@ -127,123 +118,63 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
} }
} }
var hidePathSourceCount int
for i, c := range s.Filesystem {
if !c.Valid() {
return nil, nil, fmt.Errorf("invalid filesystem at index %d", i)
}
c.Apply(params.Ops)
// fs counter
hidePathSourceCount += len(c.Host())
}
// AutoRoot is a collection of many BindMountOp internally
var autoRootEntries []fs.DirEntry
if s.AutoRoot != nil {
if d, err := os.ReadDir(s.AutoRoot.String()); err != nil {
return nil, nil, err
} else {
// autoroot counter
hidePathSourceCount += len(d)
autoRootEntries = d
}
}
hidePathSource := make([]*container.Absolute, 0, hidePathSourceCount)
// fs append
for _, c := range s.Filesystem { for _, c := range s.Filesystem {
// all entries already checked above if c == nil {
hidePathSource = append(hidePathSource, c.Host()...) continue
}
// autoroot append
if s.AutoRoot != nil {
for _, ent := range autoRootEntries {
name := ent.Name()
if container.IsAutoRootBindable(name) {
hidePathSource = append(hidePathSource, s.AutoRoot.Append(name))
}
}
}
// evaluated path, input path
hidePathSourceEval := make([][2]string, len(hidePathSource))
for i, a := range hidePathSource {
if a == nil {
// unreachable
return nil, nil, syscall.ENOTRECOVERABLE
} }
hidePathSourceEval[i] = [2]string{a.String(), a.String()} if !path.IsAbs(c.Src) {
if err := evalSymlinks(os, &hidePathSourceEval[i][0]); err != nil { return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src)
}
dest := c.Dst
if c.Dst == "" {
dest = c.Src
} else if !path.IsAbs(dest) {
return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
}
srcH := c.Src
if err := evalSymlinks(os, &srcH); err != nil {
return nil, nil, err return nil, nil, err
} }
}
for _, p := range hidePathSourceEval {
for i := range hidePaths { for i := range hidePaths {
// skip matched entries // skip matched entries
if hidePathMatch[i] { if hidePathMatch[i] {
continue continue
} }
if ok, err := deepContainsH(p[0], hidePaths[i]); err != nil { if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
return nil, nil, err return nil, nil, err
} else if ok { } else if ok {
hidePathMatch[i] = true hidePathMatch[i] = true
os.Printf("hiding path %q from %q", hidePaths[i], p[1]) os.Printf("hiding paths from %q", c.Src)
} }
} }
var flags int
if c.Write {
flags |= container.BindWritable
}
if c.Device {
flags |= container.BindDevice | container.BindWritable
}
if !c.Must {
flags |= container.BindOptional
}
params.Bind(c.Src, dest, flags)
} }
// cover matched paths // cover matched paths
for i, ok := range hidePathMatch { for i, ok := range hidePathMatch {
if ok { if ok {
if a, err := container.NewAbs(hidePaths[i]); err != nil { params.Tmpfs(hidePaths[i], 1<<13, 0755)
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 i, l := range s.Link { for _, l := range s.Link {
if l.Target == nil || l.Linkname == "" { params.Link(l[0], l[1])
return nil, nil, fmt.Errorf("invalid link at index %d", i)
}
linkname := l.Linkname
var dereference bool
if linkname[0] == '*' && path.IsAbs(linkname[1:]) {
linkname = linkname[1:]
dereference = true
}
params.Link(l.Target, linkname, dereference)
}
if !s.AutoEtc {
if s.Etc != nil {
params.Bind(s.Etc, container.AbsFHSEtc, 0)
}
} else {
if s.Etc == nil {
params.Etc(container.AbsFHSEtc, prefix)
} else {
params.Etc(s.Etc, prefix)
}
}
// no more ContainerConfig paths beyond this point
if !s.Device {
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.String()) store := state.NewMulti(seal.runDirPath)
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
@ -64,7 +64,7 @@ func (seal *outcome) Run(rs *RunState) error {
// accumulate enablements of remaining launchers // accumulate enablements of remaining launchers
for i, s := range states { for i, s := range states {
if s.Config != nil { if s.Config != nil {
rt |= s.Config.Enablements.Unwrap() rt |= s.Config.Enablements
} else { } else {
log.Printf("state entry %d does not contain config", i) log.Printf("state entry %d does not contain config", i)
} }
@ -88,7 +88,7 @@ func (seal *outcome) Run(rs *RunState) error {
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, hsuPath) cmd := exec.CommandContext(ctx, hsuPath)
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.Dir = container.FHSRoot // container init enters final working directory cmd.Dir = "/" // container init enters final working directory
// shim runs in the same session as monitor; see shim.go for behaviour // shim runs in the same session as monitor; see shim.go for behaviour
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) } cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
@ -123,15 +123,7 @@ func (seal *outcome) Run(rs *RunState) error {
// this prevents blocking forever on an early failure // this prevents blocking forever on an early failure
waitErr, setupErr := make(chan error, 1), make(chan error, 1) waitErr, setupErr := make(chan error, 1), make(chan error, 1)
go func() { waitErr <- cmd.Wait(); cancel() }() go func() { waitErr <- cmd.Wait(); cancel() }()
go func() { go func() { setupErr <- e.Encode(&shimParams{os.Getpid(), seal.container, seal.user.data, hlog.Load()}) }()
setupErr <- e.Encode(&shimParams{
os.Getpid(),
seal.waitDelay,
seal.container,
seal.user.data.String(),
hlog.Load(),
})
}()
select { select {
case err := <-setupErr: case err := <-setupErr:

View File

@ -15,7 +15,6 @@ import (
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
@ -49,8 +48,10 @@ const (
) )
var ( var (
ErrIdent = errors.New("invalid identity") ErrConfig = errors.New("no configuration to seal")
ErrName = errors.New("invalid username") ErrUser = errors.New("invalid aid")
ErrHome = errors.New("invalid home directory")
ErrName = errors.New("invalid username")
ErrXDisplay = errors.New(display + " unset") ErrXDisplay = errors.New(display + " unset")
@ -65,8 +66,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] // copied from [sys.State] response
runDirPath *container.Absolute runDirPath string
// 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
@ -78,7 +79,6 @@ type outcome struct {
sys *system.I sys *system.I
ctx context.Context ctx context.Context
waitDelay time.Duration
container *container.Params container *container.Params
env map[string]string env map[string]string
sync *os.File sync *os.File
@ -91,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 *container.Absolute sharePath string
// process-specific directory in XDG_RUNTIME_DIR, empty if unused // process-specific directory in XDG_RUNTIME_DIR, empty if unused
runtimeSharePath *container.Absolute runtimeSharePath string
seal *outcome seal *outcome
sc hst.Paths sc hst.Paths
@ -105,48 +105,48 @@ func (share *shareHost) ensureRuntimeDir() {
return return
} }
share.useRuntimeDir = true share.useRuntimeDir = true
share.seal.sys.Ensure(share.sc.RunDirPath.String(), 0700) share.seal.sys.Ensure(share.sc.RunDirPath, 0700)
share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath.String(), acl.Execute) share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath, acl.Execute)
share.seal.sys.Ensure(share.sc.RuntimePath.String(), 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset share.seal.sys.Ensure(share.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath.String(), acl.Execute) share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath, acl.Execute)
} }
// instance returns a process-specific share path within tmpdir // instance returns a process-specific share path within tmpdir
func (share *shareHost) instance() *container.Absolute { func (share *shareHost) instance() string {
if share.sharePath != nil { if share.sharePath != "" {
return share.sharePath return share.sharePath
} }
share.sharePath = share.sc.SharePath.Append(share.seal.id.String()) share.sharePath = path.Join(share.sc.SharePath, share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.sharePath.String(), 0711) share.seal.sys.Ephemeral(system.Process, share.sharePath, 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() *container.Absolute { func (share *shareHost) runtime() string {
if share.runtimeSharePath != nil { if share.runtimeSharePath != "" {
return share.runtimeSharePath return share.runtimeSharePath
} }
share.ensureRuntimeDir() share.ensureRuntimeDir()
share.runtimeSharePath = share.sc.RunDirPath.Append(share.seal.id.String()) share.runtimeSharePath = path.Join(share.sc.RunDirPath, share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath.String(), 0700) share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath, 0700)
share.seal.sys.UpdatePerm(share.runtimeSharePath.String(), acl.Execute) share.seal.sys.UpdatePerm(share.runtimeSharePath, 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 {
// identity // application id
aid *stringPair[int] aid *stringPair[int]
// target uid resolved by hid:aid // target uid resolved by fid: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 *container.Absolute data string
// app user home directory // app user home directory
home *container.Absolute home string
// passwd database username // passwd database username
username string username string
} }
@ -157,13 +157,6 @@ 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)
@ -176,7 +169,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(ErrIdent, return hlog.WrapErr(ErrUser,
fmt.Sprintf("identity %d out of range", config.Identity)) fmt.Sprintf("identity %d out of range", config.Identity))
} }
@ -193,7 +186,11 @@ 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.home == nil { if seal.user.data == "" || !path.IsAbs(seal.user.data) {
return hlog.WrapErr(ErrHome,
fmt.Sprintf("invalid home directory %q", seal.user.data))
}
if seal.user.home == "" {
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 {
@ -211,25 +208,26 @@ 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 config.Path == nil { if !path.IsAbs(config.Path) {
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 if config.Path, err = container.NewAbs(p); err != nil { } else {
return hlog.WrapErr(err, err.Error()) config.Path = p
} }
} else { } else {
config.Path = config.Shell config.Path = config.Shell
@ -241,47 +239,58 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
Net: true, Net: true,
Tty: true, Tty: true,
AutoEtc: true, AutoEtc: true,
}
// bind entries in /
if d, err := sys.ReadDir("/"); err != nil {
return err
} else {
b := make([]*hst.FilesystemConfig, 0, len(d))
for _, ent := range d {
p := "/" + ent.Name()
switch p {
case "/proc":
case "/dev":
case "/tmp":
case "/mnt":
case "/etc":
AutoRoot: container.AbsFHSRoot, default:
RootFlags: container.BindWritable, b = append(b, &hst.FilesystemConfig{Src: p, Write: true, Must: true})
}
}
conf.Filesystem = append(conf.Filesystem, b...)
} }
// hide nscd from sandbox if present
nscd := "/var/run/nscd"
if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
conf.Cover = append(conf.Cover, nscd)
}
// bind GPU stuff // bind GPU stuff
if config.Enablements.Unwrap()&(system.EX11|system.EWayland) != 0 { if config.Enablements&(system.EX11|system.EWayland) != 0 {
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}}) conf.Filesystem = append(conf.Filesystem, &hst.FilesystemConfig{Src: "/dev/dri", Device: true})
} }
// opportunistically bind kvm // opportunistically bind kvm
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("kvm"), Device: true, Optional: true}}) conf.Filesystem = append(conf.Filesystem, &hst.FilesystemConfig{Src: "/dev/kvm", Device: true})
// hide nscd from container if present
nscd := container.AbsFHSVar.Append("run/nscd")
if _, err := sys.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) {
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}})
}
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
var err error var err error
seal.container, seal.env, err = newContainer(config.Container, sys, seal.id.String(), &uid, &gid) seal.container, seal.env, err = newContainer(config.Container, sys, &uid, &gid)
seal.waitDelay = config.Container.WaitDelay
if err != nil { if err != nil {
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.String()} config.Args = []string{config.Path}
} }
seal.container.Path = config.Path seal.container.Path = config.Path
seal.container.Args = config.Args seal.container.Args = config.Args
@ -293,53 +302,69 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
} }
if !config.Container.AutoEtc {
if config.Container.Etc != "" {
seal.container.Bind(config.Container.Etc, "/etc", 0)
}
} else {
etcPath := config.Container.Etc
if etcPath == "" {
etcPath = "/etc"
}
seal.container.Etc(etcPath, seal.id.String())
}
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid // inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String()) innerRuntimeDir := path.Join("/run/user", mapuid.String())
seal.env[xdgRuntimeDir] = innerRuntimeDir.String() seal.env[xdgRuntimeDir] = innerRuntimeDir
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.String(), 0711) seal.sys.Ensure(share.sc.SharePath, 0711)
{ {
runtimeDir := share.sc.SharePath.Append("runtime") runtimeDir := path.Join(share.sc.SharePath, "runtime")
seal.sys.Ensure(runtimeDir.String(), 0700) seal.sys.Ensure(runtimeDir, 0700)
seal.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute) seal.sys.UpdatePermType(system.User, runtimeDir, acl.Execute)
runtimeDirInst := runtimeDir.Append(seal.user.aid.String()) runtimeDirInst := path.Join(runtimeDir, seal.user.aid.String())
seal.sys.Ensure(runtimeDirInst.String(), 0700) seal.sys.Ensure(runtimeDirInst, 0700)
seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute)
seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755) seal.container.Tmpfs("/run/user", 1<<12, 0755)
seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable) seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
} }
{ {
tmpdir := share.sc.SharePath.Append("tmpdir") tmpdir := path.Join(share.sc.SharePath, "tmpdir")
seal.sys.Ensure(tmpdir.String(), 0700) seal.sys.Ensure(tmpdir, 0700)
seal.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute) seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
tmpdirInst := tmpdir.Append(seal.user.aid.String()) tmpdirInst := path.Join(tmpdir, seal.user.aid.String())
seal.sys.Ensure(tmpdirInst.String(), 01700) seal.sys.Ensure(tmpdirInst, 01700)
seal.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp // mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
seal.container.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable) seal.container.Bind(tmpdirInst, "/tmp", container.BindWritable)
} }
{ {
homeDir := "/var/empty"
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, seal.user.home, container.BindWritable) seal.container.Bind(seal.user.data, homeDir, container.BindWritable)
seal.container.Dir = seal.user.home seal.container.Dir = homeDir
seal.env["HOME"] = seal.user.home.String() seal.env["HOME"] = homeDir
seal.env["USER"] = username seal.env["USER"] = username
seal.env[shell] = config.Shell.String() seal.env[shell] = config.Shell
seal.container.Place(container.AbsFHSEtc.Append("passwd"), seal.container.Place("/etc/passwd",
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+seal.user.home.String()+":"+config.Shell.String()+"\n")) []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+homeDir+":"+config.Shell+"\n"))
seal.container.Place(container.AbsFHSEtc.Append("group"), seal.container.Place("/etc/group",
[]byte("hakurei:x:"+mapgid.String()+":\n")) []byte("hakurei:x:"+mapgid.String()+":\n"))
} }
@ -348,19 +373,19 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
seal.env[term] = t seal.env[term] = t
} }
if config.Enablements.Unwrap()&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 *container.Absolute var socketPath string
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 = share.sc.RuntimePath.Append(wayland.FallbackName) socketPath = path.Join(share.sc.RuntimePath, wayland.FallbackName)
} else if a, err := container.NewAbs(name); err != nil { } else if !path.IsAbs(name) {
socketPath = share.sc.RuntimePath.Append(name) socketPath = path.Join(share.sc.RuntimePath, name)
} else { } else {
socketPath = a socketPath = name
} }
innerPath := innerRuntimeDir.Append(wayland.FallbackName) innerPath := path.Join(innerRuntimeDir, 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,36 +395,35 @@ 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 := share.instance().Append("wayland") outerPath := path.Join(share.instance(), "wayland")
seal.sys.Wayland(&seal.sync, outerPath.String(), socketPath.String(), appID, seal.id.String()) seal.sys.Wayland(&seal.sync, outerPath, socketPath, 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.String(), acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
} }
} }
if config.Enablements.Unwrap()&system.EX11 != 0 { if config.Enablements&system.EX11 != 0 {
if d, ok := sys.LookupEnv(display); !ok { if d, ok := sys.LookupEnv(display); !ok {
return hlog.WrapErr(ErrXDisplay, return hlog.WrapErr(ErrXDisplay,
"DISPLAY is not set") "DISPLAY is not set")
} else { } else {
seal.sys.ChangeHosts("#" + seal.user.uid.String()) seal.sys.ChangeHosts("#" + seal.user.uid.String())
seal.env[display] = d seal.env[display] = d
socketDir := container.AbsFHSTmp.Append(".X11-unix") seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix", 0)
seal.container.Bind(socketDir, socketDir, 0)
} }
} }
if config.Enablements.Unwrap()&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 := share.sc.RuntimePath.Append("pulse") pulseRuntimeDir := path.Join(share.sc.RuntimePath, "pulse")
// PulseAudio socket (usually `/run/user/%d/pulse/native`) // PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := pulseRuntimeDir.Append("native") pulseSocket := path.Join(pulseRuntimeDir, "native")
if _, err := sys.Stat(pulseRuntimeDir.String()); err != nil { if _, err := sys.Stat(pulseRuntimeDir); 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))
@ -408,7 +432,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.String()); err != nil { if s, err := sys.Stat(pulseSocket); 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))
@ -423,38 +447,39 @@ 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 := share.runtime().Append("pulse") innerPulseRuntimeDir := path.Join(share.runtime(), "pulse")
innerPulseSocket := innerRuntimeDir.Append("pulse", "native") innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
seal.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String()) seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
seal.env[pulseServer] = "unix:" + innerPulseSocket.String() seal.env[pulseServer] = "unix:" + innerPulseSocket
// 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.AbsTmp.Append("/pulse-cookie") innerDst := hst.Tmp + "/pulse-cookie"
seal.env[pulseCookie] = innerDst.String() seal.env[pulseCookie] = innerDst
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)
} }
} }
if config.Enablements.Unwrap()&system.EDBus != 0 { if config.Enablements&system.EDBus != 0 {
// ensure dbus session bus defaults // ensure dbus session bus defaults
if config.SessionBus == nil { if config.SessionBus == nil {
config.SessionBus = dbus.NewConfig(config.ID, true, true) config.SessionBus = dbus.NewConfig(config.ID, true, true)
} }
// downstream socket paths // downstream socket paths
sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket") sharePath := share.instance()
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.String(), systemPath.String(), sessionPath, systemPath,
); err != nil { ); err != nil {
return err return err
} else { } else {
@ -462,29 +487,30 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
// share proxy sockets // share proxy sockets
sessionInner := innerRuntimeDir.Append("bus") sessionInner := path.Join(innerRuntimeDir, "bus")
seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String() seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.container.Bind(sessionPath, sessionInner, 0) seal.container.Bind(sessionPath, sessionInner, 0)
seal.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write) seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if config.SystemBus != nil { if config.SystemBus != nil {
systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket") systemInner := "/run/dbus/system_bus_socket"
seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String() seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.container.Bind(systemPath, systemInner, 0) seal.container.Bind(systemPath, systemInner, 0)
seal.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write) seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
} }
} }
// mount root read-only as the final setup Op for _, dest := range config.Container.Cover {
seal.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY) seal.container.Tmpfs(dest, 1<<13, 0755)
}
// append ExtraPerms last // append ExtraPerms last
for _, p := range config.ExtraPerms { for _, p := range config.ExtraPerms {
if p == nil || p.Path == nil { if p == nil {
continue continue
} }
if p.Ensure { if p.Ensure {
seal.sys.Ensure(p.Path.String(), 0700) seal.sys.Ensure(p.Path, 0700)
} }
perms := make(acl.Perms, 0, 3) perms := make(acl.Perms, 0, 3)
@ -497,7 +523,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.String(), perms...) seal.sys.UpdatePermType(system.User, p.Path, perms...)
} }
// flatten and sort env for deterministic behaviour // flatten and sort env for deterministic behaviour

View File

@ -1,65 +0,0 @@
#include "shim-signal.h"
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static pid_t hakurei_shim_param_ppid = -1;
static int hakurei_shim_fd = -1;
static ssize_t hakurei_shim_write(const void *buf, size_t count) {
int savedErrno = errno;
ssize_t ret = write(hakurei_shim_fd, buf, count);
if (ret == -1 && errno != EAGAIN)
exit(EXIT_FAILURE);
errno = savedErrno;
return ret;
}
/* see shim_linux.go for handling of the value */
static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
if (sig != SIGCONT || si == NULL) {
/* unreachable */
hakurei_shim_write("\2", 1);
return;
}
if (si->si_pid == hakurei_shim_param_ppid) {
/* monitor requests shim exit */
hakurei_shim_write("\0", 1);
return;
}
/* unexpected si_pid */
hakurei_shim_write("\3", 1);
if (getppid() != hakurei_shim_param_ppid)
/* shim orphaned before monitor delivers a signal */
hakurei_shim_write("\1", 1);
}
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd) {
if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1)
*(int *)NULL = 0; /* unreachable */
struct sigaction new_action = {0}, old_action = {0};
if (sigaction(SIGCONT, NULL, &old_action) != 0)
return;
if (old_action.sa_handler != SIG_DFL) {
errno = ENOTRECOVERABLE;
return;
}
new_action.sa_sigaction = hakurei_shim_sigaction;
if (sigemptyset(&new_action.sa_mask) != 0)
return;
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
if (sigaction(SIGCONT, &new_action, NULL) != 0)
return;
errno = 0;
hakurei_shim_param_ppid = ppid;
hakurei_shim_fd = fd;
}

View File

@ -1,3 +0,0 @@
#include <signal.h>
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd);

View File

@ -3,13 +3,10 @@ package app
import ( import (
"context" "context"
"errors" "errors"
"io"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"runtime"
"sync/atomic"
"syscall" "syscall"
"time" "time"
@ -19,7 +16,55 @@ import (
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
) )
//#include "shim-signal.h" /*
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <signal.h>
static pid_t hakurei_shim_param_ppid = -1;
// this cannot unblock hlog since Go code is not async-signal-safe
static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
if (sig != SIGCONT || si == NULL) {
// unreachable
fprintf(stderr, "sigaction: sa_sigaction got invalid siginfo\n");
return;
}
// monitor requests shim exit
if (si->si_pid == hakurei_shim_param_ppid)
exit(254);
fprintf(stderr, "sigaction: got SIGCONT from process %d\n", si->si_pid);
// shim orphaned before monitor delivers a signal
if (getppid() != hakurei_shim_param_ppid)
exit(3);
}
void hakurei_shim_setup_cont_signal(pid_t ppid) {
struct sigaction new_action = {0}, old_action = {0};
if (sigaction(SIGCONT, NULL, &old_action) != 0)
return;
if (old_action.sa_handler != SIG_DFL) {
errno = ENOTRECOVERABLE;
return;
}
new_action.sa_sigaction = hakurei_shim_sigaction;
if (sigemptyset(&new_action.sa_mask) != 0)
return;
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
if (sigaction(SIGCONT, &new_action, NULL) != 0)
return;
errno = 0;
hakurei_shim_param_ppid = ppid;
}
*/
import "C" import "C"
const shimEnv = "HAKUREI_SHIM" const shimEnv = "HAKUREI_SHIM"
@ -28,10 +73,6 @@ type shimParams struct {
// monitor pid, checked against ppid in signal handler // monitor pid, checked against ppid in signal handler
Monitor int Monitor int
// duration to wait for after interrupting a container's initial process before the container is killed;
// zero value defaults to [DefaultShimWaitDelay], values exceeding [MaxShimWaitDelay] becomes [MaxShimWaitDelay]
WaitDelay time.Duration
// finalised container params // finalised container params
Container *container.Params Container *container.Params
// path to outer home directory // path to outer home directory
@ -41,16 +82,6 @@ type shimParams struct {
Verbose bool Verbose bool
} }
const (
// ShimExitRequest is returned when the monitor process requests shim exit.
ShimExitRequest = 254
// ShimExitOrphan is returned when the shim is orphaned before monitor delivers a signal.
ShimExitOrphan = 3
DefaultShimWaitDelay = 5 * time.Second
MaxShimWaitDelay = 30 * time.Second
)
// ShimMain is the main function of the shim process and runs as the unconstrained target user. // ShimMain is the main function of the shim process and runs as the unconstrained target user.
func ShimMain() { func ShimMain() {
hlog.Prepare("shim") hlog.Prepare("shim")
@ -64,7 +95,7 @@ func ShimMain() {
closeSetup func() error closeSetup func() error
) )
if f, err := container.Receive(shimEnv, &params, nil); err != nil { if f, err := container.Receive(shimEnv, &params, nil); err != nil {
if errors.Is(err, syscall.EBADF) { if errors.Is(err, container.ErrInvalid) {
log.Fatal("invalid config descriptor") log.Fatal("invalid config descriptor")
} }
if errors.Is(err, container.ErrNotSet) { if errors.Is(err, container.ErrNotSet) {
@ -75,62 +106,17 @@ func ShimMain() {
} else { } else {
internal.InstallOutput(params.Verbose) internal.InstallOutput(params.Verbose)
closeSetup = f closeSetup = f
}
var signalPipe io.ReadCloser // the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(params.Monitor)); err != nil {
if r, w, err := os.Pipe(); err != nil { log.Fatalf("cannot install SIGCONT handler: %v", err)
log.Fatalf("cannot pipe: %v", err)
} else if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(params.Monitor), C.int(w.Fd())); err != nil {
log.Fatalf("cannot install SIGCONT handler: %v", err)
} else {
defer runtime.KeepAlive(w)
signalPipe = r
}
// pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); errno != 0 {
log.Fatalf("cannot set parent-death signal: %v", errno)
}
// signal handler outcome
var cancelContainer atomic.Pointer[context.CancelFunc]
go func() {
buf := make([]byte, 1)
for {
if _, err := signalPipe.Read(buf); err != nil {
log.Fatalf("cannot read from signal pipe: %v", err)
}
switch buf[0] {
case 0: // got SIGCONT from monitor: shim exit requested
if fp := cancelContainer.Load(); params.Container.ForwardCancel && fp != nil && *fp != nil {
(*fp)()
// shim now bound by ShimWaitDelay, implemented below
continue
}
// setup has not completed, terminate immediately
hlog.Resume()
os.Exit(ShimExitRequest)
return
case 1: // got SIGCONT after adoption: monitor died before delivering signal
hlog.BeforeExit()
os.Exit(ShimExitOrphan)
return
case 2: // unreachable
log.Println("sa_sigaction got invalid siginfo")
case 3: // got SIGCONT from unexpected process: hopefully the terminal driver
log.Println("got SIGCONT from unexpected process")
default: // unreachable
log.Fatalf("got invalid message %d from signal handler", buf[0])
}
} }
}()
// pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); errno != 0 {
log.Fatalf("cannot set parent-death signal: %v", errno)
}
}
if params.Container == nil || params.Container.Ops == nil { if params.Container == nil || params.Container.Ops == nil {
log.Fatal("invalid container params") log.Fatal("invalid container params")
@ -157,19 +143,17 @@ func ShimMain() {
log.Fatalf("path %q is not a directory", params.Home) log.Fatalf("path %q is not a directory", params.Home)
} }
var name string
if len(params.Container.Args) > 0 {
name = params.Container.Args[0]
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
cancelContainer.Store(&stop) defer stop() // unreachable
z := container.New(ctx) z := container.New(ctx, name)
z.Params = *params.Container z.Params = *params.Container
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
z.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
z.WaitDelay = params.WaitDelay z.WaitDelay = 2 * time.Second
if z.WaitDelay == 0 {
z.WaitDelay = DefaultShimWaitDelay
}
if z.WaitDelay > MaxShimWaitDelay {
z.WaitDelay = MaxShimWaitDelay
}
if err := z.Start(); err != nil { if err := z.Start(); err != nil {
hlog.PrintBaseError(err, "cannot start container:") hlog.PrintBaseError(err, "cannot start container:")

View File

@ -3,11 +3,10 @@ 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"
) )
@ -51,23 +50,18 @@ 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) {
if tempDir, err := container.NewAbs(os.TempDir()); err != nil { v.SharePath = path.Join(os.TempDir(), "hakurei."+strconv.Itoa(os.Getuid()))
log.Fatalf("invalid TMPDIR: %v", err)
} else {
v.TempDir = tempDir
}
v.SharePath = v.TempDir.Append("hakurei." + strconv.Itoa(os.Getuid()))
hlog.Verbosef("process share directory at %q", v.SharePath) hlog.Verbosef("process share directory at %q", v.SharePath)
r, _ := os.LookupEnv(xdgRuntimeDir) if r, ok := os.LookupEnv(xdgRuntimeDir); !ok || r == "" || !path.IsAbs(r) {
if a, err := container.NewAbs(r); err != nil {
// fall back to path in share since hakurei has no hard XDG dependency // fall back to path in share since hakurei has no hard XDG dependency
v.RunDirPath = v.SharePath.Append("run") v.RunDirPath = path.Join(v.SharePath, "run")
v.RuntimePath = v.RunDirPath.Append("compat") v.RuntimePath = path.Join(v.RunDirPath, "compat")
} else { } else {
v.RuntimePath = a v.RuntimePath = r
v.RunDirPath = v.RuntimePath.Append("hakurei") v.RunDirPath = path.Join(v.RuntimePath, "hakurei")
} }
hlog.Verbosef("runtime directory at %q", v.RunDirPath) hlog.Verbosef("runtime directory at %q", v.RunDirPath)
} }

View File

@ -86,7 +86,7 @@ func (s *Std) Uid(aid int) (int, error) {
cmd.Path = hsuPath cmd.Path = hsuPath
cmd.Stderr = os.Stderr // pass through fatal messages cmd.Stderr = os.Stderr // pass through fatal messages
cmd.Env = []string{"HAKUREI_APP_ID=" + strconv.Itoa(aid)} cmd.Env = []string{"HAKUREI_APP_ID=" + strconv.Itoa(aid)}
cmd.Dir = container.FHSRoot cmd.Dir = "/"
var ( var (
p []byte p []byte
exitError *exec.ExitError exitError *exec.ExitError

View File

@ -12,38 +12,30 @@ import (
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
) )
const ( const lddTimeout = 2 * time.Second
lddName = "ldd"
lddTimeout = 2 * time.Second
)
var ( var (
msgStatic = []byte("Not a valid dynamic program") msgStatic = []byte("Not a valid dynamic program")
msgStaticGlibc = []byte("not a dynamic executable") msgStaticGlibc = []byte("not a dynamic executable")
) )
func Exec(ctx context.Context, p string) ([]*Entry, error) { func Exec(ctx context.Context, p string) ([]*Entry, error) { return ExecFilter(ctx, nil, nil, p) }
func ExecFilter(ctx context.Context,
commandContext func(context.Context) *exec.Cmd,
f func([]byte) []byte,
p string) ([]*Entry, error) {
c, cancel := context.WithTimeout(ctx, lddTimeout) c, cancel := context.WithTimeout(ctx, lddTimeout)
defer cancel() defer cancel()
z := container.New(c, "ldd", p)
var toolPath *container.Absolute z.CommandContext = commandContext
if s, err := exec.LookPath(lddName); err != nil { z.Hostname = "hakurei-ldd"
return nil, err
} else if toolPath, err = container.NewAbs(s); err != nil {
return nil, err
}
z := container.NewCommand(c, toolPath, lddName, p)
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. z.Bind("/", "/", 0).Proc("/proc").Dev("/dev")
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
@ -62,5 +54,8 @@ func Exec(ctx context.Context, p string) ([]*Entry, error) {
} }
v := stdout.Bytes() v := stdout.Bytes()
if f != nil {
v = f(v)
}
return Parse(v) return Parse(v)
} }

View File

@ -1,20 +1,21 @@
package ldd package ldd
import ( import (
"hakurei.app/container" "path"
"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) []*container.Absolute { func Path(entries []*Entry) []string {
p := make([]*container.Absolute, 0, len(entries)*2) p := make([]string, 0, len(entries)*2)
for _, entry := range entries { for _, entry := range entries {
if a, err := container.NewAbs(entry.Path); err == nil { if path.IsAbs(entry.Path) {
p = append(p, a.Dir()) p = append(p, path.Dir(entry.Path))
} }
if a, err := container.NewAbs(entry.Name); err == nil { if path.IsAbs(entry.Name) {
p = append(p, a.Dir()) p = append(p, path.Dir(entry.Name))
} }
} }
container.SortAbs(p) slices.Sort(p)
return container.CompactAbs(p) return slices.Compact(p)
} }

109
nixos.nix
View File

@ -82,8 +82,7 @@ in
own = [ own = [
"${id}.*" "${id}.*"
"org.mpris.MediaPlayer2.${id}.*" "org.mpris.MediaPlayer2.${id}.*"
] ] ++ ext.own;
++ ext.own;
inherit (ext) call broadcast; inherit (ext) call broadcast;
}; };
@ -102,7 +101,8 @@ in
}; };
command = if app.command == null then app.name else app.command; command = if app.command == null then app.name else app.command;
script = if app.script == null then ("exec " + command + " $@") else app.script; script = if app.script == null then ("exec " + command + " $@") else app.script;
isGraphical = if app.gpu != null then app.gpu else app.enablements.wayland || app.enablements.x11; enablements = with app.capability; (if wayland then 1 else 0) + (if x11 then 2 else 0) + (if dbus then 4 else 0) + (if pulse then 8 else 0);
isGraphical = if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11;
conf = { conf = {
path = path =
@ -115,7 +115,7 @@ in
app.path; app.path;
args = if app.args == null then [ "${app.name}-start" ] else app.args; args = if app.args == null then [ "${app.name}-start" ] else app.args;
inherit id; inherit id enablements;
inherit (dbusConfig) session_bus system_bus; inherit (dbusConfig) session_bus system_bus;
direct_wayland = app.insecureWayland; direct_wayland = app.insecureWayland;
@ -123,12 +123,10 @@ 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 enablements;
container = { container = {
inherit (app) inherit (app)
wait_delay
devel devel
userns userns
net net
@ -142,82 +140,69 @@ in
filesystem = filesystem =
let let
bind = src: { bind = src: { inherit src; };
type = "bind"; mustBind = src: {
inherit src; inherit src;
require = true;
}; };
optBind = src: { devBind = src: {
type = "bind";
inherit src;
optional = true;
};
optDevBind = src: {
type = "bind";
inherit src; inherit src;
dev = true; dev = true;
optional = true;
}; };
in in
[ [
(bind "/bin") (mustBind "/bin")
(bind "/usr/bin") (mustBind "/usr/bin")
(bind "/nix/store") (mustBind "/nix/store")
(optBind "/sys/block") (bind "/sys/block")
(optBind "/sys/bus") (bind "/sys/bus")
(optBind "/sys/class") (bind "/sys/class")
(optBind "/sys/dev") (bind "/sys/dev")
(optBind "/sys/devices") (bind "/sys/devices")
] ]
++ optionals app.nix [ ++ optionals app.nix [
(bind "/nix/var") (mustBind "/nix/var")
] ]
++ optionals isGraphical [ ++ optionals isGraphical [
(optDevBind "/dev/dri") (devBind "/dev/dri")
(optDevBind "/dev/nvidiactl") (devBind "/dev/nvidiactl")
(optDevBind "/dev/nvidia-modeset") (devBind "/dev/nvidia-modeset")
(optDevBind "/dev/nvidia-uvm") (devBind "/dev/nvidia-uvm")
(optDevBind "/dev/nvidia-uvm-tools") (devBind "/dev/nvidia-uvm-tools")
(optDevBind "/dev/nvidia0") (devBind "/dev/nvidia0")
] ]
++ optionals app.useCommonPaths cfg.commonPaths ++ optionals app.useCommonPaths cfg.commonPaths
++ app.extraPaths; ++ app.extraPaths;
auto_etc = true; auto_etc = true;
cover = [ "/var/run/nscd" ];
symlink = [ symlink =
{
target = "/run/current-system";
linkname = "*/run/current-system";
}
]
++ optionals (isGraphical && config.hardware.graphics.enable) (
[ [
{ [
target = "/run/opengl-driver"; "*/run/current-system"
linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument; "/run/current-system"
} ]
] ]
++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [ ++ optionals (isGraphical && config.hardware.graphics.enable) (
{ [
target = "/run/opengl-driver-32"; [
linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument; config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument
} "/run/opengl-driver"
] ]
); ]
++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [
[
config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument
/run/opengl-driver-32
]
]
);
}; };
};
checkedConfig = };
name: value:
let
file = pkgs.writeText name (builtins.toJSON value);
in
pkgs.runCommand "checked-${name}" { nativeBuildInputs = [ cfg.package ]; } ''
ln -vs ${file} "$out"
hakurei show ${file}
'';
in in
pkgs.writeShellScriptBin app.name '' pkgs.writeShellScriptBin app.name ''
exec hakurei${if app.verbose then " -v" else ""} app ${checkedConfig "hakurei-app-${app.name}.json" conf} $@ exec hakurei${if app.verbose then " -v" else ""} app ${pkgs.writeText "hakurei-app-${app.name}.json" (builtins.toJSON conf)} $@
'' ''
) )
] ]
@ -226,7 +211,7 @@ in
pkg = if app.share != null then app.share else pkgs.${app.name}; pkg = if app.share != null then app.share else pkgs.${app.name};
copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true"; copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true";
in in
optional (app.enablements.wayland || app.enablements.x11) ( optional (app.capability.wayland || app.capability.x11) (
pkgs.runCommand "${app.name}-share" { } '' pkgs.runCommand "${app.name}-share" { } ''
mkdir -p $out/share mkdir -p $out/share
${copy "${pkg}/share/applications"} ${copy "${pkg}/share/applications"}

View File

@ -3,6 +3,38 @@ packages:
let let
inherit (lib) types mkOption mkEnableOption; inherit (lib) types mkOption mkEnableOption;
mountPoint =
let
inherit (types)
str
submodule
nullOr
listOf
;
in
listOf (submodule {
options = {
dst = mkOption {
type = nullOr str;
default = null;
description = ''
Mount point in container, same as src if null.
'';
};
src = mkOption {
type = str;
description = ''
Host filesystem path to make available to the container.
'';
};
write = mkEnableOption "mounting path as writable";
dev = mkEnableOption "use of device files";
require = mkEnableOption "start failure if the bind mount cannot be established for any reason";
};
});
in in
{ {
@ -44,7 +76,6 @@ in
type = type =
let let
inherit (types) inherit (types)
int
ints ints
str str
bool bool
@ -164,16 +195,6 @@ in
''; '';
}; };
wait_delay = mkOption {
type = nullOr int;
default = null;
description = ''
Duration to wait for after interrupting a container's initial process in nanoseconds.
A negative value causes the container to be terminated immediately on cancellation.
Setting this to null defaults to five seconds.
'';
};
devel = mkEnableOption "debugging-related kernel interfaces"; devel = mkEnableOption "debugging-related kernel interfaces";
userns = mkEnableOption "user namespace creation"; userns = mkEnableOption "user namespace creation";
tty = mkEnableOption "access to the controlling terminal"; tty = mkEnableOption "access to the controlling terminal";
@ -205,16 +226,16 @@ in
}; };
extraPaths = mkOption { extraPaths = mkOption {
type = anything; type = mountPoint;
default = [ ]; default = [ ];
description = '' description = ''
Extra paths to make available to the container. Extra paths to make available to the container.
''; '';
}; };
enablements = { capability = {
wayland = mkOption { wayland = mkOption {
type = nullOr bool; type = bool;
default = true; default = true;
description = '' description = ''
Whether to share the Wayland socket. Whether to share the Wayland socket.
@ -222,7 +243,7 @@ in
}; };
x11 = mkOption { x11 = mkOption {
type = nullOr bool; type = bool;
default = false; default = false;
description = '' description = ''
Whether to share the X11 socket and allow connection. Whether to share the X11 socket and allow connection.
@ -230,7 +251,7 @@ in
}; };
dbus = mkOption { dbus = mkOption {
type = nullOr bool; type = bool;
default = true; default = true;
description = '' description = ''
Whether to proxy D-Bus. Whether to proxy D-Bus.
@ -238,7 +259,7 @@ in
}; };
pulse = mkOption { pulse = mkOption {
type = nullOr bool; type = bool;
default = true; default = true;
description = '' description = ''
Whether to share the PulseAudio socket and cookie. Whether to share the PulseAudio socket and cookie.
@ -263,21 +284,13 @@ in
}; };
commonPaths = mkOption { commonPaths = mkOption {
type = types.anything; type = mountPoint;
default = [ ]; default = [ ];
description = '' description = ''
Common extra paths to make available to the container. Common extra paths to make available to the container.
''; '';
}; };
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

@ -14,7 +14,7 @@
wayland-scanner, wayland-scanner,
xorg, xorg,
# for hpkg # for planterette
zstd, zstd,
gnutar, gnutar,
coreutils, coreutils,
@ -32,7 +32,7 @@
buildGoModule rec { buildGoModule rec {
pname = "hakurei"; pname = "hakurei";
version = "0.1.3"; version = "0.1.1";
srcFiltered = builtins.path { srcFiltered = builtins.path {
name = "${pname}-src"; name = "${pname}-src";
@ -84,7 +84,7 @@ buildGoModule rec {
env = { env = {
# required by libpsx # required by libpsx
CGO_LDFLAGS_ALLOW = "-Wl,(--no-whole-archive|--whole-archive)"; CGO_LDFLAGS_ALLOW = "-Wl,(--no-whole-archive|--whole-archive)";
# nix build environment does not allow acls # nix build environment does not allow acls
GO_TEST_SKIP_ACL = 1; GO_TEST_SKIP_ACL = 1;
}; };
@ -124,7 +124,7 @@ buildGoModule rec {
makeBinaryWrapper "$out/libexec/hakurei" "$out/bin/hakurei" \ makeBinaryWrapper "$out/libexec/hakurei" "$out/bin/hakurei" \
--inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages} --inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages}
makeBinaryWrapper "$out/libexec/hpkg" "$out/bin/hpkg" \ makeBinaryWrapper "$out/libexec/planterette" "$out/bin/planterette" \
--inherit-argv0 --prefix PATH : ${ --inherit-argv0 --prefix PATH : ${
lib.makeBinPath ( lib.makeBinPath (
appPackages appPackages

View File

@ -3,7 +3,6 @@ package system
import ( import (
"testing" "testing"
"hakurei.app/container"
"hakurei.app/system/acl" "hakurei.app/system/acl"
) )
@ -53,19 +52,19 @@ func TestACLString(t *testing.T) {
et Enablement et Enablement
perms []acl.Perm perms []acl.Perm
}{ }{
{`--- type: process path: "/proc/nonexistent"`, Process, []acl.Perm{}}, {`--- type: process path: "/nonexistent"`, Process, []acl.Perm{}},
{`r-- type: user path: "/proc/nonexistent"`, User, []acl.Perm{acl.Read}}, {`r-- type: user path: "/nonexistent"`, User, []acl.Perm{acl.Read}},
{`-w- type: wayland path: "/proc/nonexistent"`, EWayland, []acl.Perm{acl.Write}}, {`-w- type: wayland path: "/nonexistent"`, EWayland, []acl.Perm{acl.Write}},
{`--x type: x11 path: "/proc/nonexistent"`, EX11, []acl.Perm{acl.Execute}}, {`--x type: x11 path: "/nonexistent"`, EX11, []acl.Perm{acl.Execute}},
{`rw- type: dbus path: "/proc/nonexistent"`, EDBus, []acl.Perm{acl.Read, acl.Write}}, {`rw- type: dbus path: "/nonexistent"`, EDBus, []acl.Perm{acl.Read, acl.Write}},
{`r-x type: pulseaudio path: "/proc/nonexistent"`, EPulse, []acl.Perm{acl.Read, acl.Execute}}, {`r-x type: pulseaudio path: "/nonexistent"`, EPulse, []acl.Perm{acl.Read, acl.Execute}},
{`rwx type: user path: "/proc/nonexistent"`, User, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, {`rwx type: user path: "/nonexistent"`, User, []acl.Perm{acl.Read, acl.Write, acl.Execute}},
{`rwx type: process path: "/proc/nonexistent"`, Process, []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}}, {`rwx type: process path: "/nonexistent"`, Process, []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
a := &ACL{et: tc.et, perms: tc.perms, path: container.Nonexistent} a := &ACL{et: tc.et, perms: tc.perms, path: "/nonexistent"}
if got := a.String(); got != tc.want { if got := a.String(); got != tc.want {
t.Errorf("String() = %v, want %v", t.Errorf("String() = %v, want %v",
got, tc.want) got, tc.want)

View File

@ -1,17 +1,22 @@
package dbus_test package dbus_test
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"strings" "strings"
"syscall" "syscall"
"testing" "testing"
"time" "time"
"hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -59,23 +64,20 @@ func TestFinalise(t *testing.T) {
} }
func TestProxyStartWaitCloseString(t *testing.T) { func TestProxyStartWaitCloseString(t *testing.T) {
t.Run("sandbox", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, true) }) oldWaitDelay := helper.WaitDelay
helper.WaitDelay = 16 * time.Second
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
t.Run("sandbox", func(t *testing.T) {
proxyName := dbus.ProxyName
dbus.ProxyName = os.Args[0]
t.Cleanup(func() { dbus.ProxyName = proxyName })
testProxyFinaliseStartWaitCloseString(t, true)
})
t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) }) t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) })
} }
func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
{
oldWaitDelay := helper.WaitDelay
helper.WaitDelay = 16 * time.Second
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
}
{
proxyName := dbus.ProxyName
dbus.ProxyName = os.Args[0]
t.Cleanup(func() { dbus.ProxyName = proxyName })
}
var p *dbus.Proxy var p *dbus.Proxy
t.Run("string for nil proxy", func(t *testing.T) { t.Run("string for nil proxy", func(t *testing.T) {
@ -120,13 +122,36 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel() defer cancel()
output := new(strings.Builder)
if !useSandbox { if !useSandbox {
p = dbus.NewDirect(ctx, final, output) p = dbus.NewDirect(ctx, final, nil)
} else { } else {
p = dbus.New(ctx, final, output) p = dbus.New(ctx, final, nil)
} }
p.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
return exec.CommandContext(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperInit", "--", "init")
}
p.CmdF = func(v any) {
if useSandbox {
z := v.(*container.Container)
if z.Args[0] != dbus.ProxyName {
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
}
z.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, z.Args[1:]...)
} else {
cmd := v.(*exec.Cmd)
if cmd.Args[0] != dbus.ProxyName {
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
}
cmd.Err = nil
cmd.Path = os.Args[0]
cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args[1:]...)
}
}
p.FilterF = func(v []byte) []byte { return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1] }
output := new(strings.Builder)
t.Run("invalid wait", func(t *testing.T) { t.Run("invalid wait", func(t *testing.T) {
wantErr := "dbus: not started" wantErr := "dbus: not started"
if err := p.Wait(); err == nil || err.Error() != wantErr { if err := p.Wait(); err == nil || err.Error() != wantErr {
@ -151,9 +176,9 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
} }
t.Run("string", func(t *testing.T) { t.Run("string", func(t *testing.T) {
wantSubstr := fmt.Sprintf("%s --args=3 --fd=4", os.Args[0]) wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0])
if useSandbox { if useSandbox {
wantSubstr = `argv: ["xdg-dbus-proxy" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf` wantSubstr = fmt.Sprintf(`argv: ["%s" "-test.run=TestHelperStub" "--" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf`, os.Args[0])
} }
if got := p.String(); !strings.Contains(got, wantSubstr) { if got := p.String(); !strings.Contains(got, wantSubstr) {
t.Errorf("String: %q, want %q", t.Errorf("String: %q, want %q",
@ -178,3 +203,11 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
}) })
} }
} }
func TestHelperInit(t *testing.T) {
if len(os.Args) != 5 || os.Args[4] != "init" {
return
}
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, internal.InstallOutput)
}

View File

@ -5,6 +5,9 @@ import (
"errors" "errors"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath"
"slices"
"strconv" "strconv"
"syscall" "syscall"
@ -33,6 +36,9 @@ func (p *Proxy) Start() error {
if !p.useSandbox { if !p.useSandbox {
p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) { p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) {
if p.CmdF != nil {
p.CmdF(cmd)
}
if p.output != nil { if p.output != nil {
cmd.Stdout, cmd.Stderr = p.output, p.output cmd.Stdout, cmd.Stderr = p.output, p.output
} }
@ -40,26 +46,24 @@ func (p *Proxy) Start() error {
cmd.Env = make([]string, 0) cmd.Env = make([]string, 0)
}, nil) }, nil)
} else { } else {
var toolPath *container.Absolute toolPath := p.name
if a, err := container.NewAbs(p.name); err != nil { if filepath.Base(p.name) == p.name {
if p.name, err = exec.LookPath(p.name); err != nil { if s, err := exec.LookPath(p.name); err != nil {
return err
} else if toolPath, err = container.NewAbs(p.name); err != nil {
return err return err
} else {
toolPath = s
} }
} else {
toolPath = a
} }
var libPaths []*container.Absolute var libPaths []string
if entries, err := ldd.Exec(ctx, toolPath.String()); err != nil { if entries, err := ldd.ExecFilter(ctx, p.CommandContext, p.FilterF, toolPath); err != nil {
return err return err
} else { } else {
libPaths = ldd.Path(entries) libPaths = ldd.Path(entries)
} }
p.helper = helper.New( p.helper = helper.New(
ctx, toolPath, "xdg-dbus-proxy", ctx, toolPath,
p.final, true, p.final, true,
argF, func(z *container.Container) { argF, func(z *container.Container) {
z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompFlags |= seccomp.AllowMultiarch
@ -69,56 +73,57 @@ func (p *Proxy) Start() error {
z.ScopeAbstract = false z.ScopeAbstract = false
z.Hostname = "hakurei-dbus" z.Hostname = "hakurei-dbus"
z.CommandContext = p.CommandContext
if p.output != nil { if p.output != nil {
z.Stdout, z.Stderr = p.output, p.output z.Stdout, z.Stderr = p.output, p.output
} }
if p.CmdF != nil {
p.CmdF(z)
}
// these lib paths are unpredictable, so mount them first so they cannot cover anything // these lib paths are unpredictable, so mount them first so they cannot cover anything
for _, name := range libPaths { for _, name := range libPaths {
z.Bind(name, name, 0) z.Bind(name, name, 0)
} }
// upstream bus directories // upstream bus directories
upstreamPaths := make([]*container.Absolute, 0, 2) upstreamPaths := make([]string, 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" { if pair[0] != "path" || !path.IsAbs(pair[1]) {
continue continue
} }
if a, err := container.NewAbs(pair[1]); err != nil { upstreamPaths = append(upstreamPaths, path.Dir(pair[1]))
continue
} else {
upstreamPaths = append(upstreamPaths, a.Dir())
}
} }
} }
} }
container.SortAbs(upstreamPaths) slices.Sort(upstreamPaths)
upstreamPaths = container.CompactAbs(upstreamPaths) upstreamPaths = slices.Compact(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([]*container.Absolute, 0, 2) sockDirPaths := make([]string, 0, 2)
if a, err := container.NewAbs(p.final.Session[1]); err == nil { if d := path.Dir(p.final.Session[1]); path.IsAbs(d) {
sockDirPaths = append(sockDirPaths, a.Dir()) sockDirPaths = append(sockDirPaths, d)
} }
if a, err := container.NewAbs(p.final.System[1]); err == nil { if d := path.Dir(p.final.System[1]); path.IsAbs(d) {
sockDirPaths = append(sockDirPaths, a.Dir()) sockDirPaths = append(sockDirPaths, d)
} }
container.SortAbs(sockDirPaths) slices.Sort(sockDirPaths)
sockDirPaths = container.CompactAbs(sockDirPaths) sockDirPaths = slices.Compact(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 := toolPath.Dir() binPath := path.Dir(toolPath)
z.Bind(binPath, binPath, 0) z.Bind(binPath, binPath, 0)
}, nil) }, nil)
} }

View File

@ -1,17 +0,0 @@
package dbus_test
import (
"os"
"testing"
"hakurei.app/container"
"hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
helper.InternalHelperStub()
os.Exit(m.Run())
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"os/exec"
"sync" "sync"
"syscall" "syscall"
@ -36,6 +37,10 @@ type Proxy struct {
useSandbox bool useSandbox bool
name string name string
CmdF func(any)
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
FilterF func([]byte) []byte
mu, pmu sync.RWMutex mu, pmu sync.RWMutex
} }

9
system/dbus/stub_test.go Normal file
View File

@ -0,0 +1,9 @@
package dbus_test
import (
"testing"
"hakurei.app/helper"
)
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }

View File

@ -3,8 +3,6 @@ package system
import ( import (
"os" "os"
"testing" "testing"
"hakurei.app/container"
) )
func TestEnsure(t *testing.T) { func TestEnsure(t *testing.T) {
@ -62,11 +60,11 @@ func TestMkdirString(t *testing.T) {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
m := &Mkdir{ m := &Mkdir{
et: tc.et, et: tc.et,
path: container.Nonexistent, path: "/nonexistent",
perm: 0701, perm: 0701,
ephemeral: tc.ephemeral, ephemeral: tc.ephemeral,
} }
want := "mode: " + os.FileMode(0701).String() + " type: " + tc.want + ` path: "/proc/nonexistent"` want := "mode: " + os.FileMode(0701).String() + " type: " + tc.want + " path: \"/nonexistent\""
if got := m.String(); got != want { if got := m.String(); got != want {
t.Errorf("String() = %v, want %v", got, want) t.Errorf("String() = %v, want %v", got, want)
} }

View File

@ -82,18 +82,13 @@
jack.enable = true; jack.enable = true;
}; };
virtualisation = { virtualisation.qemu.options = [
# Hopefully reduces spurious test failures: # Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
memorySize = 4096; "-vga none -device virtio-gpu-pci"
qemu.options = [ # Increase Go test compiler performance:
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch: "-smp 8"
"-vga none -device virtio-gpu-pci" ];
# Increase Go test compiler performance:
"-smp 8"
];
};
environment.hakurei = { environment.hakurei = {
enable = true; enable = true;
@ -126,22 +121,7 @@
wayland-utils wayland-utils
]; ];
command = "foot"; command = "foot";
enablements = { capability = {
dbus = false;
pulse = false;
};
};
"cat.gensokyo.extern.foot.noEnablements.immediate" = {
name = "ne-foot-immediate";
identity = 1;
shareUid = true;
verbose = true;
wait_delay = -1;
share = pkgs.foot;
packages = [ ];
command = "foot";
enablements = {
dbus = false; dbus = false;
pulse = false; pulse = false;
}; };
@ -154,7 +134,7 @@
share = pkgs.foot; share = pkgs.foot;
packages = [ pkgs.foot ]; packages = [ pkgs.foot ];
command = "foot"; command = "foot";
enablements.dbus = false; capability.dbus = false;
}; };
"cat.gensokyo.extern.Alacritty.x11" = { "cat.gensokyo.extern.Alacritty.x11" = {
@ -171,7 +151,7 @@
mesa-demos mesa-demos
]; ];
command = "alacritty"; command = "alacritty";
enablements = { capability = {
wayland = false; wayland = false;
x11 = true; x11 = true;
dbus = false; dbus = false;
@ -192,7 +172,7 @@
wayland-utils wayland-utils
]; ];
command = "foot"; command = "foot";
enablements = { capability = {
dbus = false; dbus = false;
pulse = false; pulse = false;
}; };
@ -204,7 +184,7 @@
verbose = true; verbose = true;
share = pkgs.strace; share = pkgs.strace;
command = "strace true"; command = "strace true";
enablements = { capability = {
wayland = false; wayland = false;
x11 = false; x11 = false;
dbus = false; dbus = false;

View File

@ -1,60 +0,0 @@
{ pkgs, ... }:
{
system.stateVersion = "23.05";
users.users = {
alice = {
isNormalUser = true;
description = "Alice Foobar";
password = "foobar";
uid = 1000;
extraGroups = [ "wheel" ];
};
untrusted = {
isNormalUser = true;
description = "Untrusted user";
password = "foobar";
uid = 1001;
};
};
home-manager.users.alice.home.stateVersion = "24.11";
security = {
sudo.wheelNeedsPassword = false;
rtkit.enable = true;
};
services = {
getty.autologinUser = "alice";
pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
jack.enable = true;
};
};
environment.variables = {
SWAYSOCK = "/tmp/sway-ipc.sock";
WLR_RENDERER = "pixman";
};
programs = {
sway.enable = true;
bash.loginShellInit = ''
if [ "$(tty)" = "/dev/tty1" ]; then
set -e
mkdir -p ~/.config/sway
(sed s/Mod4/Mod1/ /etc/sway/config &&
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill') > ~/.config/sway/config
sway --validate
systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
fi
'';
};
}

View File

@ -1,25 +0,0 @@
{ pkgs, ... }:
{
environment.hakurei = {
enable = true;
stateDir = "/var/lib/hakurei";
users.alice = 0;
apps = {
"cat.gensokyo.extern.foot.noEnablements" = {
name = "ne-foot";
identity = 1;
shareUid = true;
verbose = true;
share = pkgs.foot;
packages = [ pkgs.foot ];
command = "foot";
enablements = {
dbus = false;
pulse = false;
};
};
};
extraHomeConfig.home.stateVersion = "23.05";
};
}

View File

@ -1,28 +0,0 @@
{ lib, pkgs, ... }:
let
tracing = name: "\"/sys/kernel/debug/tracing/${name}\"";
in
{
environment.systemPackages = [
(pkgs.writeShellScriptBin "hakurei-set-up-tracing" ''
set -e
echo "$1" > ${tracing "set_graph_function"}
echo function_graph > ${tracing "current_tracer"}
echo funcgraph-tail > ${tracing "trace_options"}
echo funcgraph-retval > ${tracing "trace_options"}
echo nofuncgraph-cpu > ${tracing "trace_options"}
echo nofuncgraph-overhead > ${tracing "trace_options"}
echo nofuncgraph-duration > ${tracing "trace_options"}
'')
(pkgs.writeShellScriptBin "hakurei-print-trace" "exec cat ${tracing "trace"}")
(pkgs.writeShellScriptBin "hakurei-consume-trace" "exec cat ${tracing "trace_pipe"}")
];
boot.kernelPatches = [
{
name = "funcgraph-retval";
patch = null;
extraStructuredConfig.FUNCTION_GRAPH_RETVAL = lib.kernel.yes;
}
];
}

View File

@ -1,56 +0,0 @@
{
virtualisation.vmVariant.virtualisation = {
memorySize = 4096;
qemu.options = [
"-vga none -device virtio-gpu-pci"
"-smp 8"
];
mountHostNixStore = true;
writableStore = true;
writableStoreUseTmpfs = false;
sharedDirectories = {
cwd = {
target = "/mnt/.ro-cwd";
source = ''"$OLDPWD"'';
securityModel = "none";
};
};
fileSystems = {
"/mnt/.ro-cwd".options = [
"ro"
"noatime"
];
"/mnt/cwd".overlay = {
lowerdir = [ "/mnt/.ro-cwd" ];
upperdir = "/tmp/.cwd/upper";
workdir = "/tmp/.cwd/work";
};
"/mnt/src".overlay = {
lowerdir = [ ../.. ];
upperdir = "/tmp/.src/upper";
workdir = "/tmp/.src/work";
};
};
};
systemd.services = {
logrotate-checkconf.enable = false;
hakurei-src-fix-ownership = {
wantedBy = [ "multi-user.target" ];
wants = [ "mnt-src.mount" ];
after = [ "mnt-src.mount" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
chown -R alice:users /mnt/src/
chmod -R +w /mnt/src/
'';
};
};
}

View File

@ -23,21 +23,17 @@ let
; ;
}; };
importTestCase =
path:
import path {
inherit
fs
ent
ignore
system
;
};
callTestCase = callTestCase =
path: identity: path: identity:
let let
tc = importTestCase path; tc = import path {
inherit
fs
ent
ignore
system
;
};
in in
{ {
name = "check-sandbox-${tc.name}"; name = "check-sandbox-${tc.name}";
@ -65,13 +61,9 @@ let
testCaseName = name: "cat.gensokyo.hakurei.test." + name; testCaseName = name: "cat.gensokyo.hakurei.test." + name;
in in
{ {
apps = { ${testCaseName "preset"} = callTestCase ./preset.nix 1;
${testCaseName "preset"} = callTestCase ./preset.nix 1; ${testCaseName "tty"} = callTestCase ./tty.nix 2;
${testCaseName "tty"} = callTestCase ./tty.nix 2; ${testCaseName "mapuid"} = callTestCase ./mapuid.nix 3;
${testCaseName "mapuid"} = callTestCase ./mapuid.nix 3; ${testCaseName "device"} = callTestCase ./device.nix 4;
${testCaseName "device"} = callTestCase ./device.nix 4; ${testCaseName "pdlike"} = callTestCase ./pdlike.nix 5;
${testCaseName "pdlike"} = callTestCase ./pdlike.nix 5;
};
pd = importTestCase ./pd.nix;
} }

View File

@ -47,10 +47,7 @@ in
]; ];
fs = fs "dead" { fs = fs "dead" {
".hakurei" = fs "800001ed" { ".hakurei" = fs "800001ed" { } null;
".ro-store" = fs "801001fd" null null;
store = fs "800001ff" null null;
} null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null; bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" null null; dev = fs "800001ed" null null;
etc = fs "800001ed" { etc = fs "800001ed" {
@ -176,6 +173,13 @@ in
} null; } null;
} null; } null;
".local" = fs "800001ed" { ".local" = fs "800001ed" {
share = fs "800001ed" {
dbus-1 = fs "800001ed" {
services = fs "800001ed" {
"ca.desrt.dconf.service" = fs "80001ff" null null;
} null;
} null;
} null;
state = fs "800001ed" { state = fs "800001ed" {
".keep" = fs "80001ff" null ""; ".keep" = fs "80001ff" null "";
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
@ -198,14 +202,15 @@ in
} null; } null;
} null; } null;
} null; } null;
run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
cache = fs "800001ed" { private = fs "800001c0" null null; } null; cache = fs "800001ed" { private = fs "800001c0" null null; } null;
} null; } null;
} null; } null;
mount = [ mount = [
(ent "/sysroot" "/" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004") (ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw") (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000004,gid=1000004") (ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
(ent "/" "/dev" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/" "/dev" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666") (ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/shm" "rw,nosuid,nodev" "tmpfs" "tmpfs" ignore) (ent "/" "/dev/shm" "rw,nosuid,nodev" "tmpfs" "tmpfs" ignore)
@ -221,10 +226,8 @@ in
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000004,gid=1000004") (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
(ent "/tmp/hakurei.1000/runtime/4" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/tmp/hakurei.1000/runtime/4" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.1000/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/tmp/hakurei.1000/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a4" "/var/lib/hakurei/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/var/lib/hakurei/u0/a4" "/var/lib/hakurei/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
@ -233,6 +236,7 @@ in
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore) (ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/var/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8k,mode=755,uid=1000004,gid=1000004")
]; ];
seccomp = true; seccomp = true;

View File

@ -56,10 +56,7 @@ in
]; ];
fs = fs "dead" { fs = fs "dead" {
".hakurei" = fs "800001ed" { ".hakurei" = fs "800001ed" { } null;
".ro-store" = fs "801001fd" null null;
store = fs "800001ff" null null;
} null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null; bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" { dev = fs "800001ed" {
core = fs "80001ff" null null; core = fs "80001ff" null null;
@ -202,6 +199,13 @@ in
} null; } null;
} null; } null;
".local" = fs "800001ed" { ".local" = fs "800001ed" {
share = fs "800001ed" {
dbus-1 = fs "800001ed" {
services = fs "800001ed" {
"ca.desrt.dconf.service" = fs "80001ff" null null;
} null;
} null;
} null;
state = fs "800001ed" { state = fs "800001ed" {
".keep" = fs "80001ff" null ""; ".keep" = fs "80001ff" null "";
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
@ -224,15 +228,16 @@ in
} null; } null;
} null; } null;
} null; } null;
run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
cache = fs "800001ed" { private = fs "800001c0" null null; } null; cache = fs "800001ed" { private = fs "800001c0" null null; } null;
} null; } null;
} null; } null;
mount = [ mount = [
(ent "/sysroot" "/" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003") (ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw") (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000003,gid=1000003") (ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
(ent "/" "/dev" "ro,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000003,gid=1000003") (ent "/" "/dev" "rw,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000003,gid=1000003")
(ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
@ -251,10 +256,8 @@ in
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000003,gid=1000003") (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
(ent "/tmp/hakurei.1000/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/tmp/hakurei.1000/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.1000/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/tmp/hakurei.1000/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a3" "/var/lib/hakurei/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/var/lib/hakurei/u0/a3" "/var/lib/hakurei/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
@ -263,6 +266,7 @@ in
(ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore) (ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/var/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8k,mode=755,uid=1000003,gid=1000003")
]; ];
seccomp = true; seccomp = true;

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