hst/config: move container fields from toplevel
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m7s
Test / Hpkg (push) Successful in 3m54s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Sandbox (race detector) (push) Successful in 2m10s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m33s

This change also moves pd behaviour to cmd/hakurei, as this does not belong in the hst API.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-10-07 01:50:56 +09:00
parent f280994957
commit 9e48d7f562
19 changed files with 435 additions and 336 deletions

View File

@@ -37,7 +37,35 @@ func TestApp(t *testing.T) {
}{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&hst.Config{Username: "chronos", Home: m("/home/chronos")},
&hst.Config{Container: &hst.ContainerConfig{
Userns: true, HostNet: true, HostAbstract: true, Tty: true,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{
Target: container.AbsFHSRoot,
Source: container.AbsFHSRoot,
Write: true,
Special: true,
}},
{FilesystemConfig: &hst.FSBind{
Source: container.AbsFHSDev.Append("kvm"),
Device: true,
Optional: true,
}},
{FilesystemConfig: &hst.FSBind{
Target: container.AbsFHSEtc,
Source: container.AbsFHSEtc,
Special: true,
}},
},
Username: "chronos",
Shell: m("/run/current-system/sw/bin/zsh"),
Home: m("/home/chronos"),
Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"/run/current-system/sw/bin/zsh"},
}},
state.ID{
0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15,
@@ -70,7 +98,6 @@ func TestApp(t *testing.T) {
DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777).
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
Readonly(m("/var/run/nscd"), 0755).
Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac").
Tmpfs(m("/run/user/1971"), 8192, 0755).
Tmpfs(m("/run/nscd"), 8192, 0755).
@@ -93,11 +120,8 @@ func TestApp(t *testing.T) {
"nixos permissive defaults chromium", new(stubNixOS),
&hst.Config{
ID: "org.chromium.Chromium",
Args: []string{"zsh", "-c", "exec chromium "},
Identity: 9,
Groups: []string{"video"},
Username: "chronos",
Home: m("/home/chronos"),
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
@@ -130,6 +154,41 @@ func TestApp(t *testing.T) {
Filter: true,
},
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
Container: &hst.ContainerConfig{
Userns: true, HostNet: true, HostAbstract: true, Tty: true,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{
Target: container.AbsFHSRoot,
Source: container.AbsFHSRoot,
Write: true,
Special: true,
}},
{FilesystemConfig: &hst.FSBind{
Source: container.AbsFHSDev.Append("dri"),
Device: true,
Optional: true,
}},
{FilesystemConfig: &hst.FSBind{
Source: container.AbsFHSDev.Append("kvm"),
Device: true,
Optional: true,
}},
{FilesystemConfig: &hst.FSBind{
Target: container.AbsFHSEtc,
Source: container.AbsFHSEtc,
Special: true,
}},
},
Username: "chronos",
Shell: m("/run/current-system/sw/bin/zsh"),
Home: m("/home/chronos"),
Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"zsh", "-c", "exec chromium "},
},
},
state.ID{
0xeb, 0xf0, 0x83, 0xd1,
@@ -207,7 +266,6 @@ func TestApp(t *testing.T) {
Tmpfs(m("/dev/shm"), 0, 01777).
Bind(m("/dev/dri"), m("/dev/dri"), container.BindWritable|container.BindDevice|container.BindOptional).
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
Readonly(m("/var/run/nscd"), 0755).
Etc(m("/etc/"), "ebf083d1b175911782d413369b64ce7c").
Tmpfs(m("/run/user/1971"), 8192, 0755).
Tmpfs(m("/run/nscd"), 8192, 0755).
@@ -236,10 +294,7 @@ func TestApp(t *testing.T) {
"nixos chromium direct wayland", new(stubNixOS),
&hst.Config{
ID: "org.chromium.Chromium",
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
Shell: m("/run/current-system/sw/bin/zsh"),
Container: &hst.ContainerConfig{
Userns: true, HostNet: true, MapRealUID: true, Env: nil,
Filesystem: []hst.FilesystemConfigJSON{
@@ -257,6 +312,12 @@ func TestApp(t *testing.T) {
f(&hst.FSBind{Source: m("/etc/"), Target: m("/etc/"), Special: true}),
f(&hst.FSBind{Source: m("/var/lib/persist/module/hakurei/0/1"), Write: true, Ensure: true}),
},
Username: "u0_a1",
Shell: m("/run/current-system/sw/bin/zsh"),
Home: m("/var/lib/persist/module/hakurei/0/1"),
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
},
SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
@@ -278,8 +339,6 @@ func TestApp(t *testing.T) {
},
DirectWayland: true,
Username: "u0_a1",
Home: m("/var/lib/persist/module/hakurei/0/1"),
Identity: 1, Groups: []string{},
},
state.ID{
@@ -461,7 +520,6 @@ func (s stubOsFileReadCloser) Write([]byte) (int, error) { panic("attempting to
func (s stubOsFileReadCloser) Stat() (fs.FileInfo, error) { panic("attempting to call Stat") }
type stubNixOS struct {
lookPathErr map[string]error
usernameErr map[string]error
}
@@ -617,21 +675,6 @@ func (k *stubNixOS) evalSymlinks(path string) (string, error) {
}
}
func (k *stubNixOS) lookPath(file string) (string, error) {
if k.lookPathErr != nil {
if err, ok := k.lookPathErr[file]; ok {
return "", err
}
}
switch file {
case "zsh":
return "/run/current-system/sw/bin/zsh", nil
default:
panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
}
}
func (k *stubNixOS) lookupGroupId(name string) (string, error) {
switch name {
case "video":

View File

@@ -45,9 +45,6 @@ type syscallDispatcher interface {
// evalSymlinks provides [filepath.EvalSymlinks].
evalSymlinks(path string) (string, error)
// lookPath provides exec.LookPath.
lookPath(file string) (string, error)
// lookupGroupId calls [user.LookupGroup] and returns the Gid field of the resulting [user.Group] struct.
lookupGroupId(name string) (string, error)
@@ -81,8 +78,6 @@ func (direct) tempdir() string { return os.TempDir()
func (direct) evalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) }
func (direct) lookPath(file string) (string, error) { return exec.LookPath(file) }
func (direct) lookupGroupId(name string) (gid string, err error) {
var group *user.Group
group, err = user.LookupGroup(name)

View File

@@ -18,7 +18,6 @@ func (panicDispatcher) open(string) (osFile, error) { panic("unreachab
func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
func (panicDispatcher) tempdir() string { panic("unreachable") }
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
func (panicDispatcher) lookPath(string) (string, error) { panic("unreachable") }
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
func (panicDispatcher) overflowUid(container.Msg) int { panic("unreachable") }

View File

@@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"maps"
"os"
"os/user"
@@ -66,11 +65,8 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, id *state.ID,
}
k.ctx = ctx
if config == nil {
return newWithMessage("invalid configuration")
}
if config.Home == nil {
return newWithMessage("invalid path to home directory")
if err := config.Validate(); err != nil {
return err
}
// TODO(ophestra): do not clobber during finalise
@@ -102,6 +98,7 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, id *state.ID,
}
}
// validation complete at this point
s := outcomeState{
ID: id,
Identity: config.Identity,
@@ -110,81 +107,6 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, id *state.ID,
Container: config.Container,
}
// permissive defaults
if s.Container == nil {
msg.Verbose("container configuration not supplied, PROCEED WITH CAUTION")
if config.Shell == nil {
config.Shell = container.AbsFHSRoot.Append("bin", "sh")
shell, _ := k.lookupEnv("SHELL")
if a, err := container.NewAbs(shell); err == nil {
config.Shell = a
}
}
// hsu clears the environment so resolve paths early
if config.Path == nil {
if len(config.Args) > 0 {
if p, err := k.lookPath(config.Args[0]); err != nil {
return &hst.AppError{Step: "look up executable file", Err: err}
} else if config.Path, err = container.NewAbs(p); err != nil {
return newWithMessageError(err.Error(), err)
}
} else {
config.Path = config.Shell
}
}
conf := &hst.ContainerConfig{
Userns: true,
HostNet: true,
HostAbstract: true,
Tty: true,
Filesystem: []hst.FilesystemConfigJSON{
// autoroot, includes the home directory
{FilesystemConfig: &hst.FSBind{
Target: container.AbsFHSRoot,
Source: container.AbsFHSRoot,
Write: true,
Special: true,
}},
},
}
// bind GPU stuff
if config.Enablements.Unwrap()&(hst.EX11|hst.EWayland) != 0 {
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}})
}
// opportunistically bind kvm
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("kvm"), Device: true, Optional: true}})
// hide nscd from container if present
nscd := container.AbsFHSVar.Append("run/nscd")
if _, err := k.stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) {
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}})
}
// do autoetc last
conf.Filesystem = append(conf.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: container.AbsFHSEtc,
Source: container.AbsFHSEtc,
Special: true,
}},
)
s.Container = conf
}
// late nil checks for pd behaviour
if config.Shell == nil {
return newWithMessage("invalid shell path")
}
if config.Path == nil {
return newWithMessage("invalid program path")
}
// enforce bounds and default early
if s.Container.WaitDelay <= 0 {
kp.waitDelay = hst.WaitDelayDefault
@@ -210,14 +132,14 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, id *state.ID,
{
ops := []outcomeOp{
// must run first
&spParamsOp{Path: config.Path, Args: config.Args},
&spParamsOp{},
// TODO(ophestra): move this late for #8 and #9
spFilesystemOp{},
spRuntimeOp{},
spTmpdirOp{},
&spAccountOp{Home: config.Home, Username: config.Username, Shell: config.Shell},
spAccountOp{},
}
et := config.Enablements.Unwrap()

View File

@@ -9,45 +9,38 @@ import (
)
// spAccountOp sets up user account emulation inside the container.
type spAccountOp struct {
// Inner directory to use as the home directory of the emulated user.
Home *container.Absolute
// String matching the default NAME_REGEX value from adduser to use as the username of the emulated user.
Username string
// Pathname of shell to use for the emulated user.
Shell *container.Absolute
}
type spAccountOp struct{}
func (s *spAccountOp) toSystem(*outcomeStateSys, *hst.Config) error {
func (s spAccountOp) toSystem(state *outcomeStateSys, _ *hst.Config) error {
const fallbackUsername = "chronos"
// do checks here to fail before fork/exec
if s.Home == nil || s.Shell == nil {
if state.Container == nil || state.Container.Home == nil || state.Container.Shell == nil {
// unreachable
return syscall.ENOTRECOVERABLE
}
if s.Username == "" {
s.Username = fallbackUsername
} else if !isValidUsername(s.Username) {
return newWithMessage(fmt.Sprintf("invalid user name %q", s.Username))
if state.Container.Username == "" {
state.Container.Username = fallbackUsername
} else if !isValidUsername(state.Container.Username) {
return newWithMessage(fmt.Sprintf("invalid user name %q", state.Container.Username))
}
return nil
}
func (s *spAccountOp) toContainer(state *outcomeStateParams) error {
state.params.Dir = s.Home
state.env["HOME"] = s.Home.String()
state.env["USER"] = s.Username
state.env["SHELL"] = s.Shell.String()
func (s spAccountOp) toContainer(state *outcomeStateParams) error {
state.params.Dir = state.Container.Home
state.env["HOME"] = state.Container.Home.String()
state.env["USER"] = state.Container.Username
state.env["SHELL"] = state.Container.Shell.String()
state.params.
Place(container.AbsFHSEtc.Append("passwd"),
[]byte(s.Username+":x:"+
[]byte(state.Container.Username+":x:"+
state.mapuid.String()+":"+
state.mapgid.String()+
":Hakurei:"+
s.Home.String()+":"+
s.Shell.String()+"\n")).
state.Container.Home.String()+":"+
state.Container.Shell.String()+"\n")).
Place(container.AbsFHSEtc.Append("group"),
[]byte("hakurei:x:"+state.mapgid.String()+":\n"))

View File

@@ -18,11 +18,6 @@ const varRunNscd = container.FHSVar + "run/nscd"
// spParamsOp initialises unordered fields of [container.Params] and the optional root filesystem.
// This outcomeOp is hardcoded to always run first.
type spParamsOp struct {
// Copied from the [hst.Config] field of the same name.
Path *container.Absolute `json:"path,omitempty"`
// Copied from the [hst.Config] field of the same name.
Args []string `json:"args"`
// Value of $TERM, stored during toSystem.
Term string
// Whether $TERM is set, stored during toSystem.
@@ -49,15 +44,15 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
state.params.HostNet = state.Container.HostNet
state.params.HostAbstract = state.Container.HostAbstract
if s.Path == nil {
if state.Container.Path == nil {
return newWithMessage("invalid program path")
}
state.params.Path = s.Path
state.params.Path = state.Container.Path
if len(s.Args) == 0 {
state.params.Args = []string{s.Path.String()}
if len(state.Container.Args) == 0 {
state.params.Args = []string{state.Container.Path.String()}
} else {
state.params.Args = s.Args
state.params.Args = state.Container.Args
}
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;