Compare commits

...

7 Commits

Author SHA1 Message Date
e58181a930
internal/app/paths: defer extra formatting
All checks were successful
Test / Create distribution (push) Successful in 1m14s
Test / Hakurei (push) Successful in 3m50s
Test / Hpkg (push) Successful in 4m44s
Test / Sandbox (race detector) (push) Successful in 4m51s
Test / Sandbox (push) Successful in 1m37s
Test / Hakurei (race detector) (push) Successful in 3m12s
Test / Flake checks (push) Successful in 1m41s
This reduces payload size for params to shim.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-30 00:21:26 +09:00
71e70b7b5f
internal/app/paths: do not print messages
All checks were successful
Test / Create distribution (push) Successful in 56s
Test / Sandbox (push) Successful in 2m32s
Test / Hakurei (push) Successful in 3m36s
Test / Hpkg (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m40s
Test / Sandbox (race detector) (push) Successful in 2m12s
Test / Flake checks (push) Successful in 1m32s
This change was missed while merging the rest of the logging changes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 09:30:57 +09:00
afa1a8043e
helper/proc: raise FulfillmentTimeout in tests
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m30s
Test / Hakurei (push) Successful in 3m36s
Test / Hpkg (push) Successful in 4m22s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (race detector) (push) Successful in 5m41s
Test / Flake checks (push) Successful in 1m32s
This appears to be yet another source of spurious test failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 07:44:33 +09:00
1ba1cb8865
hst/config: remove seccomp bit fields
All checks were successful
Test / Create distribution (push) Successful in 1m12s
Test / Sandbox (push) Successful in 2m46s
Test / Hpkg (push) Successful in 4m40s
Test / Sandbox (race detector) (push) Successful in 4m50s
Test / Hakurei (race detector) (push) Successful in 5m51s
Test / Hakurei (push) Successful in 2m36s
Test / Flake checks (push) Successful in 1m41s
These serve little purpose and are not friendly for use from other languages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 07:07:16 +09:00
44ba7a5f02
hst/enablement: move bits from system
All checks were successful
Test / Create distribution (push) Successful in 54s
Test / Sandbox (push) Successful in 2m33s
Test / Hakurei (push) Successful in 3m36s
Test / Hpkg (push) Successful in 4m30s
Test / Sandbox (race detector) (push) Successful in 4m48s
Test / Hakurei (race detector) (push) Successful in 5m47s
Test / Flake checks (push) Successful in 1m40s
This is part of the hst API, should not be in the implementation package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 06:34:29 +09:00
dc467493d8
internal: remove hlog
All checks were successful
Test / Create distribution (push) Successful in 1m11s
Test / Sandbox (push) Successful in 2m37s
Test / Hpkg (push) Successful in 4m41s
Test / Sandbox (race detector) (push) Successful in 4m53s
Test / Hakurei (race detector) (push) Successful in 5m53s
Test / Hakurei (push) Successful in 2m44s
Test / Flake checks (push) Successful in 1m48s
This package has been fully replaced by container.Msg.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 06:21:04 +09:00
46cd3a28c8
container: remove global msg
All checks were successful
Test / Create distribution (push) Successful in 1m10s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m44s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m2s
Test / Flake checks (push) Successful in 1m47s
This frees all container instances of side effects.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 06:11:47 +09:00
87 changed files with 1405 additions and 1163 deletions

View File

@ -17,17 +17,29 @@ import (
"hakurei.app/internal"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
func buildCommand(ctx context.Context, out io.Writer) command.Command {
func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var (
flagVerbose bool
flagJSON bool
)
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
c := command.New(out, log.Printf, "hakurei", func([]string) error {
msg.SwapVerbose(flagVerbose)
if early.yamaLSM != nil {
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", early.yamaLSM)
// not fatal
}
if early.dumpable != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", early.dumpable)
// not fatal
}
return nil
}).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
@ -39,10 +51,10 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
}
// config extraArgs...
config := tryPath(args[0])
config := tryPath(msg, args[0])
config.Args = append(config.Args, args[1:]...)
app.Main(ctx, config)
app.Main(ctx, msg, config)
panic("unreachable")
})
@ -78,9 +90,9 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
passwd *user.User
passwdOnce sync.Once
passwdFunc = func() {
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustID(), flagIdentity))
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustIDMsg(msg), flagIdentity))
if u, err := user.LookupId(us); err != nil {
hlog.Verbosef("cannot look up uid %s", us)
msg.Verbosef("cannot look up uid %s", us)
passwd = &user.User{
Uid: us,
Gid: us,
@ -115,18 +127,18 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
config.Home = a
}
var e system.Enablement
var e hst.Enablement
if flagWayland {
e |= system.EWayland
e |= hst.EWayland
}
if flagX11 {
e |= system.EX11
e |= hst.EX11
}
if flagDBus {
e |= system.EDBus
e |= hst.EDBus
}
if flagPulse {
e |= system.EPulse
e |= hst.EPulse
}
config.Enablements = hst.NewEnablements(e)
@ -162,7 +174,7 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
}
}
app.Main(ctx, config)
app.Main(ctx, msg, config)
panic("unreachable")
}).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
@ -202,9 +214,9 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
case 1: // instance
name := args[0]
config, entry := tryShort(name)
config, entry := tryShort(msg, name)
if config == nil {
config = tryPath(name)
config = tryPath(msg, name)
}
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON)
@ -219,8 +231,8 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
var flagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error {
var sc hst.Paths
app.CopyPaths(&sc, new(app.Hsu).MustID())
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sc.RunDirPath.String()), flagShort, flagJSON)
app.CopyPaths().Copy(&sc, new(app.Hsu).MustID())
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(msg, sc.RunDirPath.String()), flagShort, flagJSON)
return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
}

View File

@ -7,6 +7,7 @@ import (
"testing"
"hakurei.app/command"
"hakurei.app/container"
)
func TestHelp(t *testing.T) {
@ -68,7 +69,7 @@ Flags:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
out := new(bytes.Buffer)
c := buildCommand(t.Context(), out)
c := buildCommand(t.Context(), container.NewMsg(nil), new(earlyHardeningErrs), out)
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
t.Errorf("Parse: error = %v; want %v",
err, command.ErrHelp)

View File

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

View File

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

View File

@ -22,7 +22,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
defer t.MustFlush()
info := &hst.Info{User: new(app.Hsu).MustID()}
app.CopyPaths(&info.Paths, info.User)
app.CopyPaths().Copy(&info.Paths, info.User)
if flagJSON {
printJSON(output, short, info)

View File

@ -259,8 +259,6 @@ App
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
@ -415,8 +413,6 @@ App
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
@ -625,8 +621,6 @@ func Test_printPs(t *testing.T) {
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,

View File

@ -6,7 +6,6 @@ import (
"os"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/system/dbus"
)
@ -92,6 +91,7 @@ func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, arg
Device: app.Device,
Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID,
Multiarch: app.Multiarch,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
@ -113,12 +113,6 @@ func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, arg
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
}
if app.Multiarch {
config.Container.SeccompFlags |= seccomp.AllowMultiarch
}
if app.Bluetooth {
config.Container.SeccompFlags |= seccomp.AllowBluetooth
}
return config
}

View File

@ -13,22 +13,21 @@ import (
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
var (
errSuccess = errors.New("success")
)
func init() {
hlog.Prepare("hpkg")
func main() {
log.SetPrefix("hpkg: ")
log.SetFlags(0)
msg := container.NewMsg(log.Default())
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)
}
}
func main() {
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
@ -41,7 +40,7 @@ func main() {
flagVerbose 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, "hpkg", func([]string) error { msg.SwapVerbose(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
@ -90,12 +89,12 @@ func main() {
}
cleanup := func() {
// should be faster than a native implementation
mustRun(chmod, "-R", "+w", workDir.String())
mustRun(rm, "-rf", workDir.String())
mustRun(msg, chmod, "-R", "+w", workDir.String())
mustRun(msg, rm, "-rf", workDir.String())
}
beforeRunFail.Store(&cleanup)
mustRun(tar, "-C", workDir.String(), "-xf", pkgPath)
mustRun(msg, tar, "-C", workDir.String(), "-xf", pkgPath)
/*
Parse bundle and app metadata, do pre-install checks.
@ -148,10 +147,10 @@ func main() {
}
// sec: should compare version string
hlog.Verbosef("installing application %q version %q over local %q",
msg.Verbosef("installing application %q version %q over local %q",
bundle.ID, bundle.Version, a.Version)
} else {
hlog.Verbosef("application %q clean installation", bundle.ID)
msg.Verbosef("application %q clean installation", bundle.ID)
// sec: should install credentials
}
@ -159,7 +158,7 @@ func main() {
Setup steps for files owned by the target user.
*/
withCacheDir(ctx, "install", []string{
withCacheDir(ctx, msg, "install", []string{
// export inner bundle path in the environment
"export BUNDLE=" + hst.Tmp + "/bundle",
// replace inner /etc
@ -181,7 +180,7 @@ func main() {
}, workDir, bundle, pathSet, flagDropShell, cleanup)
if bundle.GPU {
withCacheDir(ctx, "mesa-wrappers", []string{
withCacheDir(ctx, msg, "mesa-wrappers", []string{
// link nixGL mesa wrappers
"mkdir -p nix/.nixGL",
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
@ -193,7 +192,7 @@ func main() {
Activate home-manager generation.
*/
withNixDaemon(ctx, "activate", []string{
withNixDaemon(ctx, msg, "activate", []string{
// clean up broken links
"mkdir -p .local/state/{nix,home-manager}",
"chmod -R +w .local/state/{nix,home-manager}",
@ -261,7 +260,7 @@ func main() {
*/
if a.GPU && flagAutoDrivers {
withNixDaemon(ctx, "nix-gl", []string{
withNixDaemon(ctx, msg, "nix-gl", []string{
"mkdir -p /nix/.nixGL/auto",
"rm -rf /nix/.nixGL/auto",
"export NIXPKGS_ALLOW_UNFREE=1",
@ -316,7 +315,7 @@ func main() {
Spawn app.
*/
mustRunApp(ctx, config, func() {})
mustRunApp(ctx, msg, config, func() {})
return errSuccess
}).
Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
@ -324,9 +323,9 @@ func main() {
}
c.MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err)
msg.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
hlog.BeforeExit()
msg.BeforeExit()
os.Exit(0)
}
})

View File

@ -9,7 +9,6 @@ import (
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
const bash = "bash"
@ -51,8 +50,8 @@ func lookPath(file string) string {
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(name string, arg ...string) {
hlog.Verbosef("spawning process: %q %q", name, arg)
func mustRun(msg container.Msg, name string, arg ...string) {
msg.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 {

View File

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

View File

@ -2,20 +2,20 @@ package main
import (
"context"
"os"
"strings"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/internal"
)
func withNixDaemon(
ctx context.Context,
msg container.Msg,
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
mustRunAppDropShell(ctx, updateConfig(&hst.Config{
mustRunAppDropShell(ctx, msg, updateConfig(&hst.Config{
ID: app.ID,
Path: pathShell,
@ -42,11 +42,11 @@ func withNixDaemon(
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
HostNet: net,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
HostNet: net,
Multiarch: true,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
@ -61,9 +61,10 @@ func withNixDaemon(
func withCacheDir(
ctx context.Context,
msg container.Msg,
action string, command []string, workDir *container.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &hst.Config{
mustRunAppDropShell(ctx, msg, &hst.Config{
ID: app.ID,
Path: pathShell,
@ -81,9 +82,9 @@ func withCacheDir(
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Hostname: formatHostname(app.Name) + "-" + action,
Multiarch: true,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: workDir.Append(container.FHSEtc), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
@ -97,12 +98,13 @@ func withCacheDir(
}, dropShell, beforeFail)
}
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
func mustRunAppDropShell(ctx context.Context, msg container.Msg, config *hst.Config, dropShell bool, beforeFail func()) {
if dropShell {
config.Args = []string{bash, "-l"}
mustRunApp(ctx, config, beforeFail)
mustRunApp(ctx, msg, config, beforeFail)
beforeFail()
internal.Exit(0)
msg.BeforeExit()
os.Exit(0)
}
mustRunApp(ctx, config, beforeFail)
mustRunApp(ctx, msg, config, beforeFail)
}

View File

@ -34,7 +34,7 @@ func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
r.resolved = make([]*BindMountOp, 0, len(d))
for _, ent := range d {
name := ent.Name()
if IsAutoRootBindable(name) {
if IsAutoRootBindable(state, name) {
// careful: the Valid method is skipped, make sure this is always valid
op := &BindMountOp{
Source: r.Host.Append(name),
@ -78,7 +78,7 @@ func (r *AutoRootOp) String() string {
}
// IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot.
func IsAutoRootBindable(name string) bool {
func IsAutoRootBindable(msg Msg, name string) bool {
switch name {
case "proc", "dev", "tmp", "mnt", "etc":

View File

@ -180,19 +180,26 @@ func TestIsAutoRootBindable(t *testing.T) {
testCases := []struct {
name string
want bool
log bool
}{
{"proc", false},
{"dev", false},
{"tmp", false},
{"mnt", false},
{"etc", false},
{"", false},
{"proc", false, false},
{"dev", false, false},
{"tmp", false, false},
{"mnt", false, false},
{"etc", false, false},
{"", false, true},
{"var", true},
{"var", true, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := IsAutoRootBindable(tc.name); got != tc.want {
var msg Msg
if tc.log {
msg = &kstub{nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),
}})}
}
if got := IsAutoRootBindable(msg, tc.name); got != tc.want {
t.Errorf("IsAutoRootBindable: %v, want %v", got, tc.want)
}
})

View File

@ -49,6 +49,7 @@ type (
cmd *exec.Cmd
ctx context.Context
msg Msg
Params
}
@ -162,10 +163,10 @@ func (p *Container) Start() error {
// map to overflow id to work around ownership checks
if p.Uid < 1 {
p.Uid = OverflowUid()
p.Uid = OverflowUid(p.msg)
}
if p.Gid < 1 {
p.Gid = OverflowGid()
p.Gid = OverflowGid(p.msg)
}
if !p.RetainSession {
@ -263,19 +264,19 @@ func (p *Container) Start() error {
}
return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
} else {
msg.Verbosef("landlock abi version %d", abi)
p.msg.Verbosef("landlock abi version %d", abi)
}
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
return &StartError{true, "create landlock ruleset", err, false, false}
} else {
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd)
return &StartError{true, "enforce landlock ruleset", err, false, false}
}
if err = Close(rulesetFd); err != nil {
msg.Verbosef("cannot close landlock ruleset: %v", err)
p.msg.Verbosef("cannot close landlock ruleset: %v", err)
// not fatal
}
}
@ -283,7 +284,7 @@ func (p *Container) Start() error {
landlockOut:
}
msg.Verbose("starting container init")
p.msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil {
return &StartError{false, "start container init", err, false, true}
}
@ -325,7 +326,7 @@ func (p *Container) Serve() error {
Getuid(),
Getgid(),
len(p.ExtraFiles),
msg.IsVerbose(),
p.msg.IsVerbose(),
},
)
if err != nil {
@ -392,17 +393,21 @@ func (p *Container) ProcessState() *os.ProcessState {
}
// New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context) *Container {
p := &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
func New(ctx context.Context, msg Msg) *Container {
if msg == nil {
msg = NewMsg(nil)
}
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}
c, cancel := context.WithCancel(ctx)
p.cancel = cancel
p.cmd = exec.CommandContext(c, MustExecutable())
p.cmd = exec.CommandContext(c, MustExecutable(msg))
return p
}
// 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)
func NewCommand(ctx context.Context, msg Msg, pathname *Absolute, name string, args ...string) *Container {
z := New(ctx, msg)
z.Path = pathname
z.Args = append([]string{name}, args...)
return z

View File

@ -23,8 +23,6 @@ import (
"hakurei.app/container/seccomp"
"hakurei.app/container/vfs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
)
@ -351,8 +349,6 @@ var containerTestCases = []struct {
}
func TestContainer(t *testing.T) {
replaceOutput(t)
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled
wantExitCode := 0
@ -547,7 +543,8 @@ func testContainerCancel(
}
func TestContainerString(t *testing.T) {
c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
msg := container.NewMsg(nil)
c := container.NewCommand(t.Context(), msg, container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
c.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset(
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
@ -689,7 +686,7 @@ var (
var helperCommands []func(c command.Command)
func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
container.TryArgv0(nil)
if os.Getenv(envDoCheck) == "1" {
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
@ -712,12 +709,13 @@ func TestMain(m *testing.M) {
}
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) {
c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...)
msg := container.NewMsg(nil)
c = container.NewCommand(ctx, msg, 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 {
if entries, err := ldd.Exec(ctx, msg, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
*libPaths = ldd.Path(entries)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,25 +11,9 @@ import (
)
func TestInitEntrypoint(t *testing.T) {
assertPrefix := func(prefix string) {
if prefix != "init" {
t.Fatalf("prepareLogger: prefix = %q", prefix)
}
}
assertVerboseFalse := func(verbose bool) {
if verbose {
t.Fatal("setVerbose: verbose = true, want false")
}
}
assertVerboseTrue := func(verbose bool) {
if !verbose {
t.Fatal("setVerbose: verbose = false, want true")
}
}
checkSimple(t, "initEntrypoint", []simpleTestCase{
{"getpid", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"getpid", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1<<10, nil),
@ -37,7 +21,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"receive bad fd", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"receive bad fd", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -47,7 +31,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"receive not set", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"receive not set", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -57,7 +41,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"receive payload decode", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"receive payload decode", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -67,7 +51,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"receive invalid params", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"receive invalid params", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -91,7 +75,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"setDumpable user", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"setDumpable user", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -112,13 +96,14 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(78), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, stub.UniqueError(77)),
call("fatalf", stub.ExpectArgs{"cannot set SUID_DUMP_USER: %v", []any{stub.UniqueError(77)}}, nil, nil),
},
}, nil},
{"writeFile uid_map", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"writeFile uid_map", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -139,6 +124,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(76), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, stub.UniqueError(75)),
@ -146,7 +132,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"writeFile setgroups", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"writeFile setgroups", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -167,6 +153,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(74), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -175,7 +162,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"writeFile gid_map", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"writeFile gid_map", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -196,6 +183,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(72), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -205,7 +193,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"setDumpable disable", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"setDumpable disable", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -226,6 +214,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(70), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -236,7 +225,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"sethostname", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"sethostname", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -257,6 +246,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(68), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -269,7 +259,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"mount rslave root", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"mount rslave root", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -290,6 +280,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(66), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -304,7 +295,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"nil op", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"nil op", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -325,6 +316,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(64), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -341,7 +333,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"invalid op", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"invalid op", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -362,6 +354,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(63), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -378,7 +371,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"early unhandled error", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"early unhandled error", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -399,6 +392,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(62), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -416,7 +410,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"early", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"early", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -437,6 +431,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(60), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -454,7 +449,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"mount ih", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"mount ih", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -475,6 +470,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(59), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -493,7 +489,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"chdir ih", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"chdir ih", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -514,6 +510,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(57), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -533,7 +530,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"mkdir sysroot", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"mkdir sysroot", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -554,6 +551,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(55), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -574,7 +572,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"mount bind sysroot", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"mount bind sysroot", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -595,6 +593,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(53), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -616,7 +615,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"mkdir host", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"mkdir host", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -637,6 +636,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(51), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -659,7 +659,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"pivotRoot ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"pivotRoot ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -680,6 +680,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(49), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -703,7 +704,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"chdir ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"chdir ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -724,6 +725,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(47), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -748,7 +750,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"apply unhandled error", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"apply unhandled error", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -769,6 +771,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(45), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -802,7 +805,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"apply", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"apply", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -823,6 +826,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(43), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -856,7 +860,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"mount rprivate host", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"mount rprivate host", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -877,6 +881,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(42), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -911,7 +916,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"unmount host", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"unmount host", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -932,6 +937,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(40), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -967,7 +973,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"open ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"open ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -988,6 +994,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(38), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -1025,7 +1032,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"chdir sysroot", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"chdir sysroot", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -1046,6 +1053,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(36), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -1084,7 +1092,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"pivotRoot sysroot", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"pivotRoot sysroot", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -1105,6 +1113,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(34), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -1144,7 +1153,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"fchdir ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"fchdir ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -1165,6 +1174,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(32), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -1205,7 +1215,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"unmount ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"unmount ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -1226,6 +1236,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(30), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -1267,7 +1278,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"chdir ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"chdir ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -1288,6 +1299,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(28), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -1330,7 +1342,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"close ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"close ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -1351,6 +1363,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(26), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -1394,7 +1407,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"capAmbientClearAll", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"capAmbientClearAll", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -1415,6 +1428,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(24), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -1459,7 +1473,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"capBoundingSetDrop", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"capBoundingSetDrop", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -1480,6 +1494,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(22), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -1532,7 +1547,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"capAmbientRaise", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"capAmbientRaise", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -1553,6 +1568,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(20), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -1638,7 +1654,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"capset", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"capset", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -1659,6 +1675,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(18), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -1745,7 +1762,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"seccompLoad", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"seccompLoad", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -1766,6 +1783,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(16), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -1854,7 +1872,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"start", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseFalse); return nil }, stub.Expect{
{"start", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
@ -1874,6 +1892,7 @@ func TestInitEntrypoint(t *testing.T) {
SeccompDisable: true,
ParentPerm: 0750,
}, 1971, 127, 2, false}, uintptr(0x39)}, stub.UniqueError(13), nil),
call("swapVerbose", stub.ExpectArgs{false}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("16777216 1971 1\n"), os.FileMode(0)}, nil, nil),
@ -1966,7 +1985,7 @@ func TestInitEntrypoint(t *testing.T) {
},
}, nil},
{"lowlastcap signaled cancel forward error", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseFalse); return nil }, stub.Expect{
{"lowlastcap signaled cancel forward error", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
/* entrypoint */
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
@ -1987,6 +2006,7 @@ func TestInitEntrypoint(t *testing.T) {
SeccompDisable: true,
ParentPerm: 0750,
}, 1971, 127, 2, false}, uintptr(0x39)}, stub.UniqueError(10), nil),
call("swapVerbose", stub.ExpectArgs{false}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("16777216 1971 1\n"), os.FileMode(0)}, nil, nil),
@ -2039,7 +2059,7 @@ func TestInitEntrypoint(t *testing.T) {
call("umask", stub.ExpectArgs{022}, 0, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("suspend", stub.ExpectArgs{}, nil, nil),
call("suspend", stub.ExpectArgs{}, true, nil),
call("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(10)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{syscall.SIGINT, syscall.SIGTERM}}, nil, nil),
@ -2065,7 +2085,7 @@ func TestInitEntrypoint(t *testing.T) {
}}},
}, nil},
{"lowlastcap signaled cancel", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseFalse); return nil }, stub.Expect{
{"lowlastcap signaled cancel", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
/* entrypoint */
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
@ -2086,6 +2106,7 @@ func TestInitEntrypoint(t *testing.T) {
SeccompDisable: true,
ParentPerm: 0750,
}, 1971, 127, 2, false}, uintptr(0x39)}, stub.UniqueError(7), nil),
call("swapVerbose", stub.ExpectArgs{false}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("16777216 1971 1\n"), os.FileMode(0)}, nil, nil),
@ -2138,7 +2159,7 @@ func TestInitEntrypoint(t *testing.T) {
call("umask", stub.ExpectArgs{022}, 0, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("suspend", stub.ExpectArgs{}, nil, nil),
call("suspend", stub.ExpectArgs{}, true, nil),
call("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{syscall.SIGINT, syscall.SIGTERM}}, nil, nil),
@ -2155,7 +2176,7 @@ func TestInitEntrypoint(t *testing.T) {
}}},
}, nil},
{"lowlastcap signaled timeout", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseFalse); return nil }, stub.Expect{
{"lowlastcap signaled timeout", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
/* entrypoint */
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
@ -2176,6 +2197,7 @@ func TestInitEntrypoint(t *testing.T) {
SeccompDisable: true,
ParentPerm: 0750,
}, 1971, 127, 2, false}, uintptr(0x39)}, stub.UniqueError(5), nil),
call("swapVerbose", stub.ExpectArgs{false}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("16777216 1971 1\n"), os.FileMode(0)}, nil, nil),
@ -2228,7 +2250,7 @@ func TestInitEntrypoint(t *testing.T) {
call("umask", stub.ExpectArgs{022}, 0, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("suspend", stub.ExpectArgs{}, nil, nil),
call("suspend", stub.ExpectArgs{}, true, nil),
call("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(5)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{syscall.SIGINT, syscall.SIGTERM}}, nil, nil),
@ -2247,7 +2269,7 @@ func TestInitEntrypoint(t *testing.T) {
}}},
}, nil},
{"lowlastcap signaled", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseFalse); return nil }, stub.Expect{
{"lowlastcap signaled", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
/* entrypoint */
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
@ -2268,6 +2290,7 @@ func TestInitEntrypoint(t *testing.T) {
SeccompDisable: true,
ParentPerm: 0750,
}, 1971, 127, 2, false}, uintptr(0x39)}, stub.UniqueError(3), nil),
call("swapVerbose", stub.ExpectArgs{false}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("16777216 1971 1\n"), os.FileMode(0)}, nil, nil),
@ -2320,7 +2343,7 @@ func TestInitEntrypoint(t *testing.T) {
call("umask", stub.ExpectArgs{022}, 0, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("suspend", stub.ExpectArgs{}, nil, nil),
call("suspend", stub.ExpectArgs{}, true, nil),
call("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(3)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{syscall.SIGINT, syscall.SIGTERM}}, nil, nil),
@ -2346,7 +2369,7 @@ func TestInitEntrypoint(t *testing.T) {
}}},
}, nil},
{"strangewait nopriv notty noseccomp yamafault", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseFalse); return nil }, stub.Expect{
{"strangewait nopriv notty noseccomp yamafault", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
/* entrypoint */
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
@ -2367,6 +2390,7 @@ func TestInitEntrypoint(t *testing.T) {
SeccompDisable: true,
ParentPerm: 0750,
}, 1971, 127, 2, false}, uintptr(0x39)}, stub.UniqueError(1), nil),
call("swapVerbose", stub.ExpectArgs{false}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("16777216 1971 1\n"), os.FileMode(0)}, nil, nil),
@ -2455,7 +2479,7 @@ func TestInitEntrypoint(t *testing.T) {
call("umask", stub.ExpectArgs{022}, 0, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("suspend", stub.ExpectArgs{}, nil, nil),
call("suspend", stub.ExpectArgs{}, true, nil),
call("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{syscall.SIGINT, syscall.SIGTERM}}, nil, nil),
@ -2481,7 +2505,7 @@ func TestInitEntrypoint(t *testing.T) {
}}},
}, nil},
{"success", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{
{"success", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
/* entrypoint */
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
@ -2503,6 +2527,7 @@ func TestInitEntrypoint(t *testing.T) {
RetainSession: true,
Privileged: true,
}, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(0), nil),
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil),
@ -2594,7 +2619,7 @@ func TestInitEntrypoint(t *testing.T) {
call("umask", stub.ExpectArgs{022}, 0, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{MustAbs("/bin/zsh")}}, nil, nil),
call("start", stub.ExpectArgs{"/bin/zsh", []string{"zsh", "-c", "exec vim"}, []string{"DISPLAY=:0"}, "/.hakurei"}, &os.Process{Pid: 0xcafe}, nil),
call("suspend", stub.ExpectArgs{}, nil, nil),
call("suspend", stub.ExpectArgs{}, true, nil),
call("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(0)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{syscall.SIGINT, syscall.SIGTERM}}, nil, nil),

View File

@ -59,7 +59,7 @@ func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
}
}
func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error {
if b.sourceFinal == nil {
if b.Flags&BindOptional == 0 {
// unreachable
@ -92,11 +92,11 @@ func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
}
if b.sourceFinal.String() == b.Target.String() {
k.verbosef("mounting %q flags %#x", target, flags)
state.Verbosef("mounting %q flags %#x", target, flags)
} else {
k.verbosef("mounting %q on %q flags %#x", source, target, flags)
state.Verbosef("mounting %q on %q flags %#x", source, target, flags)
}
return k.bindMount(source, target, flags)
return k.bindMount(state, source, target, flags)
}
func (b *BindMountOp) Is(op Op) bool {

View File

@ -46,6 +46,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
return err
}
if err := k.bindMount(
state,
toHost(FHSDev+name),
targetPath,
0,
@ -93,6 +94,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
if name, err := k.readlink(hostProc.stdout()); err != nil {
return err
} else if err = k.bindMount(
state,
toHost(name),
consolePath,
0,
@ -116,7 +118,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
return nil
}
if err := k.remount(target, MS_RDONLY); err != nil {
if err := k.remount(state, target, MS_RDONLY); err != nil {
return err
}
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)

View File

@ -52,6 +52,7 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
if err := k.ensureFile(target, 0444, state.ParentPerm); err != nil {
return err
} else if err = k.bindMount(
state,
tmpPath,
target,
syscall.MS_RDONLY|syscall.MS_NODEV,

View File

@ -21,8 +21,8 @@ type RemountOp struct {
func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil }
func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
func (r *RemountOp) apply(_ *setupState, k syscallDispatcher) error {
return k.remount(toSysroot(r.Target.String()), r.Flags)
func (r *RemountOp) apply(state *setupState, k syscallDispatcher) error {
return k.remount(state, toSysroot(r.Target.String()), r.Flags)
}
func (r *RemountOp) Is(op Op) bool {

View File

@ -96,18 +96,17 @@ const (
)
// bindMount mounts source on target and recursively applies flags if MS_REC is set.
func (p *procPaths) bindMount(source, target string, flags uintptr) error {
func (p *procPaths) bindMount(msg Msg, source, target string, flags uintptr) error {
// syscallDispatcher.bindMount and procPaths.remount must not be called from this function
if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
return err
}
return p.k.remount(target, flags)
return p.k.remount(msg, target, flags)
}
// remount applies flags on target, recursively if MS_REC is set.
func (p *procPaths) remount(target string, flags uintptr) error {
func (p *procPaths) remount(msg Msg, target string, flags uintptr) error {
// syscallDispatcher methods bindMount, remount must not be called from this function
var targetFinal string
@ -116,7 +115,7 @@ func (p *procPaths) remount(target string, flags uintptr) error {
} else {
targetFinal = v
if targetFinal != target {
p.k.verbosef("target resolves to %q", targetFinal)
msg.Verbosef("target resolves to %q", targetFinal)
}
}
@ -146,7 +145,7 @@ func (p *procPaths) remount(target string, flags uintptr) error {
return err
}
if err = remountWithFlags(p.k, n, mf); err != nil {
if err = remountWithFlags(p.k, msg, n, mf); err != nil {
return err
}
if flags&MS_REC == 0 {
@ -159,7 +158,7 @@ func (p *procPaths) remount(target string, flags uintptr) error {
continue
}
if err = remountWithFlags(p.k, cur, mf); err != nil && !errors.Is(err, EACCES) {
if err = remountWithFlags(p.k, msg, cur, mf); err != nil && !errors.Is(err, EACCES) {
return err
}
}
@ -169,12 +168,12 @@ func (p *procPaths) remount(target string, flags uintptr) error {
}
// remountWithFlags remounts mount point described by [vfs.MountInfoNode].
func remountWithFlags(k syscallDispatcher, n *vfs.MountInfoNode, mf uintptr) error {
func remountWithFlags(k syscallDispatcher, msg Msg, n *vfs.MountInfoNode, mf uintptr) error {
// syscallDispatcher methods bindMount, remount must not be called from this function
kf, unmatched := n.Flags()
if len(unmatched) != 0 {
k.verbosef("unmatched vfs options: %q", unmatched)
msg.Verbosef("unmatched vfs options: %q", unmatched)
}
if kf&mf != mf {

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ package container
import (
"bytes"
"log"
"os"
"strconv"
"sync"
@ -22,26 +21,28 @@ const (
kernelCapLastCapPath = FHSProcSys + "kernel/cap_last_cap"
)
func mustReadSysctl() {
if v, err := os.ReadFile(kernelOverflowuidPath); err != nil {
log.Fatalf("cannot read %q: %v", kernelOverflowuidPath, err)
} else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
log.Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err)
}
func mustReadSysctl(msg Msg) {
sysctlOnce.Do(func() {
if v, err := os.ReadFile(kernelOverflowuidPath); err != nil {
msg.GetLogger().Fatalf("cannot read %q: %v", kernelOverflowuidPath, err)
} else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err)
}
if v, err := os.ReadFile(kernelOverflowgidPath); err != nil {
log.Fatalf("cannot read %q: %v", kernelOverflowgidPath, err)
} else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
log.Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err)
}
if v, err := os.ReadFile(kernelOverflowgidPath); err != nil {
msg.GetLogger().Fatalf("cannot read %q: %v", kernelOverflowgidPath, err)
} else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err)
}
if v, err := os.ReadFile(kernelCapLastCapPath); err != nil {
log.Fatalf("cannot read %q: %v", kernelCapLastCapPath, err)
} else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
log.Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err)
}
if v, err := os.ReadFile(kernelCapLastCapPath); err != nil {
msg.GetLogger().Fatalf("cannot read %q: %v", kernelCapLastCapPath, err)
} else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err)
}
})
}
func OverflowUid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowuid }
func OverflowGid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowgid }
func LastCap() uintptr { sysctlOnce.Do(mustReadSysctl); return uintptr(kernelCapLastCap) }
func OverflowUid(msg Msg) int { mustReadSysctl(msg); return kernelOverflowuid }
func OverflowGid(msg Msg) int { mustReadSysctl(msg); return kernelOverflowgid }
func LastCap(msg Msg) uintptr { mustReadSysctl(msg); return uintptr(kernelCapLastCap) }

View File

@ -16,6 +16,7 @@ import (
// New initialises a Helper instance with wt as the null-terminated argument writer.
func New(
ctx context.Context,
msg container.Msg,
pathname *container.Absolute, name string,
wt io.WriterTo,
stat bool,
@ -26,7 +27,7 @@ func New(
var args []string
h := new(helperContainer)
h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
h.Container = container.NewCommand(ctx, pathname, name, args...)
h.Container = container.NewCommand(ctx, msg, pathname, name, args...)
h.WaitDelay = WaitDelay
if cmdF != nil {
cmdF(h.Container)

View File

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

View File

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

View File

@ -6,12 +6,10 @@ import (
"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)
container.TryArgv0(nil)
helper.InternalHelperStub()
os.Exit(m.Run())
}

View File

@ -4,7 +4,6 @@ import (
"time"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/system/dbus"
)
@ -66,10 +65,6 @@ type (
// 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

View File

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

View File

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

View File

@ -7,8 +7,6 @@ import (
"os"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
@ -71,7 +69,7 @@ func Template() *Config {
"--ozone-platform=wayland",
},
Enablements: NewEnablements(system.EWayland | system.EDBus | system.EPulse),
Enablements: NewEnablements(EWayland | EDBus | EPulse),
SessionBus: &dbus.Config{
See: nil,
@ -107,19 +105,17 @@ func Template() *Config {
Groups: []string{"video", "dialout", "plugdev"},
Container: &ContainerConfig{
Hostname: "localhost",
Devel: true,
Userns: true,
HostNet: true,
HostAbstract: true,
Device: true,
WaitDelay: -1,
SeccompFlags: seccomp.AllowMultiarch,
SeccompPresets: seccomp.PresetExt,
SeccompCompat: true,
Tty: true,
Multiarch: true,
MapRealUID: true,
Hostname: "localhost",
Devel: true,
Userns: true,
HostNet: true,
HostAbstract: true,
Device: true,
WaitDelay: -1,
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{

View File

@ -166,8 +166,6 @@ func TestTemplate(t *testing.T) {
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,

View File

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

View File

@ -6,6 +6,8 @@ import (
"log"
"os/exec"
"os/user"
"hakurei.app/container"
)
type stubNixOS struct {
@ -187,10 +189,10 @@ func (k *stubNixOS) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
}
}
func (k *stubNixOS) overflowUid() int { return 65534 }
func (k *stubNixOS) overflowGid() int { return 65534 }
func (k *stubNixOS) overflowUid(container.Msg) int { return 65534 }
func (k *stubNixOS) overflowGid(container.Msg) int { return 65534 }
func (k *stubNixOS) mustHsuPath() string { return "/proc/nonexistent/hsu" }
func (k *stubNixOS) mustHsuPath() *container.Absolute { return m("/proc/nonexistent/hsu") }
func (k *stubNixOS) fatalf(format string, v ...any) { panic(fmt.Sprintf(format, v...)) }

View File

@ -37,7 +37,7 @@ func TestApp(t *testing.T) {
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
system.New(context.TODO(), 1000000).
system.New(context.TODO(), container.NewMsg(nil), 1000000).
Ensure("/tmp/hakurei.0", 0711).
Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute).
Ensure("/tmp/hakurei.0/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/0", acl.Read, acl.Write, acl.Execute).
@ -121,7 +121,7 @@ func TestApp(t *testing.T) {
},
Filter: true,
},
Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse),
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
},
state.ID{
0xeb, 0xf0, 0x83, 0xd1,
@ -129,7 +129,7 @@ func TestApp(t *testing.T) {
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(context.TODO(), 1000009).
system.New(context.TODO(), container.NewMsg(nil), 1000009).
Ensure("/tmp/hakurei.0", 0711).
Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute).
Ensure("/tmp/hakurei.0/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/9", acl.Read, acl.Write, acl.Execute).
@ -229,7 +229,7 @@ func TestApp(t *testing.T) {
&hst.Config{
ID: "org.chromium.Chromium",
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse),
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
Shell: m("/run/current-system/sw/bin/zsh"),
Container: &hst.ContainerConfig{
@ -280,7 +280,7 @@ func TestApp(t *testing.T) {
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
},
system.New(context.TODO(), 1000001).
system.New(context.TODO(), container.NewMsg(nil), 1000001).
Ensure("/tmp/hakurei.0", 0711).
Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute).
Ensure("/tmp/hakurei.0/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/1", acl.Read, acl.Write, acl.Execute).
@ -288,7 +288,7 @@ func TestApp(t *testing.T) {
Ensure("/tmp/hakurei.0/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir/1", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
UpdatePermType(hst.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
@ -377,7 +377,7 @@ func TestApp(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Run("finalise", func(t *testing.T) {
seal := outcome{syscallDispatcher: tc.k, id: &stringPair[state.ID]{tc.id, tc.id.String()}}
err := seal.finalise(t.Context(), tc.config)
err := seal.finalise(t.Context(), container.NewMsg(nil), tc.config)
if err != nil {
if s, ok := container.GetErrorMessage(err); !ok {
t.Fatalf("Seal: error = %v", err)

View File

@ -20,6 +20,7 @@ const preallocateOpsCount = 1 << 5
// newContainer initialises [container.Params] via [hst.ContainerConfig].
// Note that remaining container setup must be queued by the caller.
func newContainer(
msg container.Msg,
k syscallDispatcher,
s *hst.ContainerConfig,
prefix string,
@ -31,12 +32,10 @@ func newContainer(
}
params := &container.Params{
Hostname: s.Hostname,
SeccompFlags: s.SeccompFlags,
SeccompPresets: s.SeccompPresets,
RetainSession: s.Tty,
HostNet: s.HostNet,
HostAbstract: s.HostAbstract,
Hostname: s.Hostname,
RetainSession: s.Tty,
HostNet: s.HostNet,
HostAbstract: s.HostAbstract,
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
// this behaviour is implemented in the shim
@ -73,8 +72,8 @@ func newContainer(
params.Gid = k.getgid()
*gid = params.Gid
} else {
*uid = k.overflowUid()
*gid = k.overflowGid()
*uid = k.overflowUid(msg)
*gid = k.overflowGid(msg)
}
filesystem := s.Filesystem
@ -126,11 +125,11 @@ func newContainer(
// get parent dir of socket
dir := path.Dir(pair[1])
if dir == "." || dir == container.FHSRoot {
k.verbosef("dbus socket %q is in an unusual location", pair[1])
msg.Verbosef("dbus socket %q is in an unusual location", pair[1])
}
hidePaths = append(hidePaths, dir)
} else {
k.verbosef("dbus socket %q is not absolute", pair[1])
msg.Verbosef("dbus socket %q is not absolute", pair[1])
}
}
}
@ -138,7 +137,7 @@ func newContainer(
}
hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths {
if err := evalSymlinks(k, &hidePaths[i]); err != nil {
if err := evalSymlinks(msg, k, &hidePaths[i]); err != nil {
return nil, nil, err
}
}
@ -178,7 +177,7 @@ func newContainer(
if autoroot != nil {
for _, ent := range autoRootEntries {
name := ent.Name()
if container.IsAutoRootBindable(name) {
if container.IsAutoRootBindable(msg, name) {
hidePathSource = append(hidePathSource, autoroot.Source.Append(name))
}
}
@ -193,7 +192,7 @@ func newContainer(
}
hidePathSourceEval[i] = [2]string{a.String(), a.String()}
if err := evalSymlinks(k, &hidePathSourceEval[i][0]); err != nil {
if err := evalSymlinks(msg, k, &hidePathSourceEval[i][0]); err != nil {
return nil, nil, err
}
}
@ -209,7 +208,7 @@ func newContainer(
return nil, nil, err
} else if ok {
hidePathMatch[i] = true
k.verbosef("hiding path %q from %q", hidePaths[i], p[1])
msg.Verbosef("hiding path %q from %q", hidePaths[i], p[1])
}
}
}
@ -241,12 +240,12 @@ func newContainer(
}
// evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist].
func evalSymlinks(k syscallDispatcher, v *string) error {
func evalSymlinks(msg container.Msg, k syscallDispatcher, v *string) error {
if p, err := k.evalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
k.verbosef("path %q does not yet exist", *v)
msg.Verbosef("path %q does not yet exist", *v)
} else {
*v = p
}

View File

@ -9,7 +9,6 @@ import (
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
@ -45,19 +44,15 @@ type syscallDispatcher interface {
cmdOutput(cmd *exec.Cmd) ([]byte, error)
// overflowUid provides [container.OverflowUid].
overflowUid() int
overflowUid(msg container.Msg) int
// overflowGid provides [container.OverflowGid].
overflowGid() int
overflowGid(msg container.Msg) int
// mustHsuPath provides [internal.MustHsuPath].
mustHsuPath() string
mustHsuPath() *container.Absolute
// fatalf provides [log.Fatalf].
fatalf(format string, v ...any)
isVerbose() bool
verbose(v ...any)
verbosef(format string, v ...any)
}
// direct implements syscallDispatcher on the current kernel.
@ -87,13 +82,9 @@ func (direct) lookupGroupId(name string) (gid string, err error) {
func (direct) cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() }
func (direct) overflowUid() int { return container.OverflowUid() }
func (direct) overflowGid() int { return container.OverflowGid() }
func (direct) overflowUid(msg container.Msg) int { return container.OverflowUid(msg) }
func (direct) overflowGid(msg container.Msg) int { return container.OverflowGid(msg) }
func (direct) mustHsuPath() string { return internal.MustHsuPath() }
func (direct) mustHsuPath() *container.Absolute { return internal.MustHsuPath() }
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }
func (k direct) isVerbose() bool { return hlog.Load() }
func (direct) verbose(v ...any) { hlog.Verbose(v...) }
func (direct) verbosef(format string, v ...any) { hlog.Verbosef(format, v...) }

View File

@ -0,0 +1,26 @@
package app
import (
"os"
"os/exec"
"hakurei.app/container"
)
type panicDispatcher struct{}
func (panicDispatcher) new(func(k syscallDispatcher)) { panic("unreachable") }
func (panicDispatcher) getuid() int { panic("unreachable") }
func (panicDispatcher) getgid() int { panic("unreachable") }
func (panicDispatcher) lookupEnv(string) (string, bool) { panic("unreachable") }
func (panicDispatcher) stat(string) (os.FileInfo, error) { panic("unreachable") }
func (panicDispatcher) 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") }
func (panicDispatcher) overflowGid(container.Msg) int { panic("unreachable") }
func (panicDispatcher) mustHsuPath() *container.Absolute { panic("unreachable") }
func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") }

View File

@ -20,7 +20,6 @@ import (
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system"
"hakurei.app/system/acl"
"hakurei.app/system/dbus"
@ -120,7 +119,7 @@ type hsuUser struct {
username string
}
func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
func (k *outcome) finalise(ctx context.Context, msg container.Msg, config *hst.Config) error {
const (
home = "HOME"
shell = "SHELL"
@ -183,7 +182,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
} else if !isValidUsername(k.user.username) {
return newWithMessage(fmt.Sprintf("invalid user name %q", k.user.username))
}
k.user.uid = newInt(HsuUid(hsu.MustID(), k.user.identity.unwrap()))
k.user.uid = newInt(HsuUid(hsu.MustIDMsg(msg), k.user.identity.unwrap()))
k.user.supp = make([]string, len(config.Groups))
for i, name := range config.Groups {
@ -201,7 +200,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
// permissive defaults
if config.Container == nil {
hlog.Verbose("container configuration not supplied, PROCEED WITH CAUTION")
msg.Verbose("container configuration not supplied, PROCEED WITH CAUTION")
if config.Shell == nil {
config.Shell = container.AbsFHSRoot.Append("bin", "sh")
@ -242,7 +241,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
}
// bind GPU stuff
if config.Enablements.Unwrap()&(system.EX11|system.EWayland) != 0 {
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
@ -276,13 +275,14 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
// TODO(ophestra): revert this after params to shim
share := &shareHost{seal: k}
copyPaths(k.syscallDispatcher, &share.sc, hsu.MustID())
copyPaths(k.syscallDispatcher).Copy(&share.sc, hsu.MustIDMsg(msg))
msg.Verbosef("process share directory at %q, runtime directory at %q", share.sc.SharePath, share.sc.RunDirPath)
var mapuid, mapgid *stringPair[int]
{
var uid, gid int
var err error
k.container, k.env, err = newContainer(k, config.Container, k.id.String(), &share.sc, &uid, &gid)
k.container, k.env, err = newContainer(msg, k, config.Container, k.id.String(), &share.sc, &uid, &gid)
k.waitDelay = config.Container.WaitDelay
if err != nil {
return &hst.AppError{Step: "initialise container configuration", Err: err}
@ -307,7 +307,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
k.env[xdgSessionType] = "tty"
k.runDirPath = share.sc.RunDirPath
k.sys = system.New(k.ctx, k.user.uid.unwrap())
k.sys = system.New(k.ctx, msg, k.user.uid.unwrap())
k.sys.Ensure(share.sc.SharePath.String(), 0711)
{
@ -353,11 +353,11 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
k.env[term] = t
}
if config.Enablements.Unwrap()&system.EWayland != 0 {
if config.Enablements.Unwrap()&hst.EWayland != 0 {
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath *container.Absolute
if name, ok := k.lookupEnv(wayland.WaylandDisplay); !ok {
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
msg.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
socketPath = share.sc.RuntimePath.Append(wayland.FallbackName)
} else if a, err := container.NewAbs(name); err != nil {
socketPath = share.sc.RuntimePath.Append(name)
@ -379,14 +379,14 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
k.sys.Wayland(&k.sync, outerPath.String(), socketPath.String(), appID, k.id.String())
k.container.Bind(outerPath, innerPath, 0)
} else { // bind mount wayland socket (insecure)
hlog.Verbose("direct wayland access, PROCEED WITH CAUTION")
msg.Verbose("direct wayland access, PROCEED WITH CAUTION")
share.ensureRuntimeDir()
k.container.Bind(socketPath, innerPath, 0)
k.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute)
k.sys.UpdatePermType(hst.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute)
}
}
if config.Enablements.Unwrap()&system.EX11 != 0 {
if config.Enablements.Unwrap()&hst.EX11 != 0 {
if d, ok := k.lookupEnv(display); !ok {
return newWithMessage("DISPLAY is not set")
} else {
@ -410,7 +410,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err}
}
} else {
k.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute)
k.sys.UpdatePermType(hst.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute)
if !config.Container.HostAbstract {
d = "unix:" + socketPath.String()
}
@ -423,7 +423,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
}
}
if config.Enablements.Unwrap()&system.EPulse != 0 {
if config.Enablements.Unwrap()&hst.EPulse != 0 {
// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
pulseRuntimeDir := share.sc.RuntimePath.Append("pulse")
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
@ -520,14 +520,14 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
k.container.PlaceP(innerDst, &payload)
k.sys.CopyFile(payload, paCookiePath.String(), 256, 256)
} else {
hlog.Verbose("cannot locate PulseAudio cookie (tried " +
msg.Verbose("cannot locate PulseAudio cookie (tried " +
"$PULSE_COOKIE, " +
"$XDG_CONFIG_HOME/pulse/cookie, " +
"$HOME/.pulse-cookie)")
}
}
if config.Enablements.Unwrap()&system.EDBus != 0 {
if config.Enablements.Unwrap()&hst.EDBus != 0 {
// ensure dbus session bus defaults
if config.SessionBus == nil {
config.SessionBus = dbus.NewConfig(config.ID, true, true)
@ -596,8 +596,8 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error {
}
slices.Sort(k.container.Env)
if hlog.Load() {
hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
if msg.IsVerbose() {
msg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
k.user.uid, k.user.username, config.Groups, k.container.Args, len(*k.container.Ops))
}

View File

@ -11,7 +11,6 @@ import (
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
// Hsu caches responses from cmd/hsu.
@ -40,7 +39,7 @@ func (h *Hsu) ID() (int, error) {
h.ensureDispatcher()
h.idOnce.Do(func() {
h.id = -1
hsuPath := h.k.mustHsuPath()
hsuPath := h.k.mustHsuPath().String()
cmd := exec.Command(hsuPath)
cmd.Path = hsuPath
@ -71,7 +70,10 @@ func (h *Hsu) ID() (int, error) {
}
// MustID calls [Hsu.ID] and terminates on error.
func (h *Hsu) MustID() int {
func (h *Hsu) MustID() int { return h.MustIDMsg(nil) }
// MustIDMsg implements MustID with a custom [container.Msg].
func (h *Hsu) MustIDMsg(msg container.Msg) int {
id, err := h.ID()
if err == nil {
return id
@ -79,7 +81,9 @@ func (h *Hsu) MustID() int {
const fallback = "cannot retrieve user id from setuid wrapper:"
if errors.Is(err, ErrHsuAccess) {
hlog.Verbose("*"+fallback, err)
if msg != nil {
msg.Verbose("*"+fallback, err)
}
os.Exit(1)
return -0xdeadbeef
} else if m, ok := container.GetErrorMessage(err); ok {

View File

@ -7,30 +7,53 @@ import (
"hakurei.app/hst"
)
// CopyPaths populates a [hst.Paths] struct.
func CopyPaths(v *hst.Paths, userid int) { copyPaths(direct{}, v, userid) }
// EnvPaths holds paths copied from the environment and is used to create [hst.Paths].
type EnvPaths struct {
// TempDir is returned by [os.TempDir].
TempDir *container.Absolute
// RuntimePath is copied from $XDG_RUNTIME_DIR.
RuntimePath *container.Absolute
}
// copyPaths populates a [hst.Paths] struct.
func copyPaths(k syscallDispatcher, v *hst.Paths, userid int) {
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
if tempDir, err := container.NewAbs(k.tempdir()); err != nil {
k.fatalf("invalid TMPDIR: %v", err)
} else {
v.TempDir = tempDir
// Copy expands [EnvPaths] into [hst.Paths].
func (env *EnvPaths) Copy(v *hst.Paths, userid int) {
if env == nil || env.TempDir == nil || v == nil {
panic("attempting to use an invalid EnvPaths")
}
v.SharePath = v.TempDir.Append("hakurei." + strconv.Itoa(userid))
k.verbosef("process share directory at %q", v.SharePath)
v.TempDir = env.TempDir
v.SharePath = env.TempDir.Append("hakurei." + strconv.Itoa(userid))
r, _ := k.lookupEnv(xdgRuntimeDir)
if a, err := container.NewAbs(r); err != nil {
if env.RuntimePath == nil {
// fall back to path in share since hakurei has no hard XDG dependency
v.RunDirPath = v.SharePath.Append("run")
v.RuntimePath = v.RunDirPath.Append("compat")
} else {
v.RuntimePath = a
v.RunDirPath = v.RuntimePath.Append("hakurei")
v.RuntimePath = env.RuntimePath
v.RunDirPath = env.RuntimePath.Append("hakurei")
}
k.verbosef("runtime directory at %q", v.RunDirPath)
}
// CopyPaths returns a populated [EnvPaths].
func CopyPaths() *EnvPaths { return copyPaths(direct{}) }
// copyPaths returns a populated [EnvPaths].
func copyPaths(k syscallDispatcher) *EnvPaths {
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
var env EnvPaths
if tempDir, err := container.NewAbs(k.tempdir()); err != nil {
k.fatalf("invalid TMPDIR: %v", err)
panic("unreachable")
} else {
env.TempDir = tempDir
}
r, _ := k.lookupEnv(xdgRuntimeDir)
if a, err := container.NewAbs(r); err == nil {
env.RuntimePath = a
}
return &env
}

129
internal/app/paths_test.go Normal file
View File

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

View File

@ -13,9 +13,9 @@ import (
"time"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system"
)
@ -39,6 +39,7 @@ type mainState struct {
cmdWait chan error
k *outcome
container.Msg
uintptr
}
@ -55,7 +56,7 @@ func (ms mainState) beforeExit(isFault bool) {
panic("attempting to call beforeExit twice")
}
ms.done = true
defer hlog.BeforeExit()
defer ms.BeforeExit()
if isFault && ms.cancel != nil {
ms.cancel()
@ -97,37 +98,37 @@ func (ms mainState) beforeExit(isFault bool) {
}
}
if hlog.Load() {
if ms.IsVerbose() {
if !ok {
if err != nil {
hlog.Verbosef("wait: %v", err)
ms.Verbosef("wait: %v", err)
}
} else {
switch {
case wstatus.Exited():
hlog.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus())
ms.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus())
case wstatus.CoreDump():
hlog.Verbosef("process %d dumped core", ms.cmd.Process.Pid)
ms.Verbosef("process %d dumped core", ms.cmd.Process.Pid)
case wstatus.Signaled():
hlog.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal())
ms.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal())
default:
hlog.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus)
ms.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus)
}
}
}
case <-waitDone:
hlog.Resume()
ms.Resume()
// this is only reachable when shim did not exit within shimWaitTimeout, after its WaitDelay has elapsed.
// This is different from the container failing to terminate within its timeout period, as that is enforced
// by the shim. This path is instead reached when there is a lockup in shim preventing it from completing.
log.Printf("process %d did not terminate", ms.cmd.Process.Pid)
}
hlog.Resume()
ms.Resume()
if ms.k.sync != nil {
if err := ms.k.sync.Close(); err != nil {
perror(err, "close wayland security context")
@ -146,7 +147,7 @@ func (ms mainState) beforeExit(isFault bool) {
}
}
var rt system.Enablement
var rt hst.Enablement
if states, err := c.Load(); err != nil {
// it is impossible to continue from this point;
// revert per-process state here to limit damage
@ -170,7 +171,7 @@ func (ms mainState) beforeExit(isFault bool) {
if l := len(states); l == 0 {
ec |= system.User
} else {
hlog.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
ms.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
}
// accumulate enablements of remaining launchers
@ -182,10 +183,10 @@ func (ms mainState) beforeExit(isFault bool) {
}
}
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
if hlog.Load() {
ec |= rt ^ (hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse)
if ms.IsVerbose() {
if ec > 0 {
hlog.Verbose("reverting operations scope", system.TypeString(ec))
ms.Verbose("reverting operations scope", system.TypeString(ec))
}
}
@ -219,7 +220,7 @@ func (ms mainState) fatal(fallback string, ferr error) {
}
// main carries out outcome and terminates. main does not return.
func (k *outcome) main() {
func (k *outcome) main(msg container.Msg) {
if !k.active.CompareAndSwap(false, true) {
panic("outcome: attempted to run twice")
}
@ -228,19 +229,19 @@ func (k *outcome) main() {
hsuPath := internal.MustHsuPath()
// ms.beforeExit required beyond this point
ms := &mainState{k: k}
ms := &mainState{Msg: msg, k: k}
if err := k.sys.Commit(); err != nil {
ms.fatal("cannot commit system setup:", err)
}
ms.uintptr |= mainNeedsRevert
ms.store = state.NewMulti(k.runDirPath.String())
ms.store = state.NewMulti(msg, k.runDirPath.String())
ctx, cancel := context.WithCancel(k.ctx)
defer cancel()
ms.cancel = cancel
ms.cmd = exec.CommandContext(ctx, hsuPath)
ms.cmd = exec.CommandContext(ctx, hsuPath.String())
ms.cmd.Stdin, ms.cmd.Stdout, ms.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
ms.cmd.Dir = container.FHSRoot // container init enters final working directory
// shim runs in the same session as monitor; see shim.go for behaviour
@ -260,13 +261,13 @@ func (k *outcome) main() {
}
if len(k.user.supp) > 0 {
hlog.Verbosef("attaching supplementary group ids %s", k.user.supp)
msg.Verbosef("attaching supplementary group ids %s", k.user.supp)
// interpreted by hsu
ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(k.user.supp, " "))
}
hlog.Verbosef("setuid helper at %s", hsuPath)
hlog.Suspend()
msg.Verbosef("setuid helper at %s", hsuPath)
msg.Suspend()
if err := ms.cmd.Start(); err != nil {
ms.fatal("cannot start setuid wrapper:", err)
}
@ -286,18 +287,18 @@ func (k *outcome) main() {
os.Getpid(),
k.waitDelay,
k.container,
hlog.Load(),
msg.IsVerbose(),
})
}()
return
}():
if err != nil {
hlog.Resume()
msg.Resume()
ms.fatal("cannot transmit shim config:", err)
}
case <-ctx.Done():
hlog.Resume()
msg.Resume()
ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err()))
}

View File

@ -15,8 +15,6 @@ import (
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
//#include "shim-signal.h"
@ -53,7 +51,9 @@ const (
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
func ShimMain() {
hlog.Prepare("shim")
log.SetPrefix("shim: ")
log.SetFlags(0)
msg := container.NewMsg(log.Default())
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
@ -73,7 +73,7 @@ func ShimMain() {
log.Fatalf("cannot receive shim setup params: %v", err)
} else {
internal.InstallOutput(params.Verbose)
msg.SwapVerbose(params.Verbose)
closeSetup = f
}
@ -111,12 +111,12 @@ func ShimMain() {
}
// setup has not completed, terminate immediately
hlog.Resume()
msg.Resume()
os.Exit(ShimExitRequest)
return
case 1: // got SIGCONT after adoption: monitor died before delivering signal
hlog.BeforeExit()
msg.BeforeExit()
os.Exit(ShimExitOrphan)
return
@ -144,7 +144,7 @@ func ShimMain() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
cancelContainer.Store(&stop)
z := container.New(ctx)
z := container.New(ctx, msg)
z.Params = *params.Container
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr

View File

@ -13,8 +13,8 @@ import (
"sync"
"syscall"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
// fine-grained locking and access
@ -24,16 +24,17 @@ type multiStore struct {
// initialised backends
backends *sync.Map
lock sync.RWMutex
msg container.Msg
mu sync.RWMutex
}
func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) {
s.lock.RLock()
defer s.lock.RUnlock()
s.mu.RLock()
defer s.mu.RUnlock()
// load or initialise new backend
b := new(multiBackend)
b.lock.Lock()
b.mu.Lock()
if v, ok := s.backends.LoadOrStore(identity, b); ok {
b = v.(*multiBackend)
} else {
@ -52,7 +53,7 @@ func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) {
} else {
b.lockfile = l
}
b.lock.Unlock()
b.mu.Unlock()
}
// lock backend
@ -85,17 +86,17 @@ func (s *multiStore) List() ([]int, error) {
for _, e := range entries {
// skip non-directories
if !e.IsDir() {
hlog.Verbosef("skipped non-directory entry %q", e.Name())
s.msg.Verbosef("skipped non-directory entry %q", e.Name())
continue
}
// skip non-numerical names
if v, err := strconv.Atoi(e.Name()); err != nil {
hlog.Verbosef("skipped non-aid entry %q", e.Name())
s.msg.Verbosef("skipped non-aid entry %q", e.Name())
continue
} else {
if v < 0 || v > 9999 {
hlog.Verbosef("skipped out of bounds entry %q", e.Name())
s.msg.Verbosef("skipped out of bounds entry %q", e.Name())
continue
}
@ -107,8 +108,8 @@ func (s *multiStore) List() ([]int, error) {
}
func (s *multiStore) Close() error {
s.lock.Lock()
defer s.lock.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
var errs []error
s.backends.Range(func(_, value any) bool {
@ -126,7 +127,7 @@ type multiBackend struct {
// created/opened by prepare
lockfile *os.File
lock sync.RWMutex
mu sync.RWMutex
}
func (b *multiBackend) filename(id *ID) string {
@ -169,8 +170,8 @@ func (b *multiBackend) unlockFile() error {
// reads all launchers in simpleBackend
// file contents are ignored if decode is false
func (b *multiBackend) load(decode bool) (Entries, error) {
b.lock.RLock()
defer b.lock.RUnlock()
b.mu.RLock()
defer b.mu.RUnlock()
// read directory contents, should only contain files named after ids
var entries []os.DirEntry
@ -280,8 +281,8 @@ func (b *multiBackend) decodeState(r io.ReadSeeker, state *State) error {
// Save writes process state to filesystem
func (b *multiBackend) Save(state *State, configWriter io.WriterTo) error {
b.lock.Lock()
defer b.lock.Unlock()
b.mu.Lock()
defer b.mu.Unlock()
if configWriter == nil && state.Config == nil {
return ErrNoConfig
@ -336,8 +337,8 @@ func (b *multiBackend) encodeState(w io.WriteSeeker, state *State, configWriter
}
func (b *multiBackend) Destroy(id ID) error {
b.lock.Lock()
defer b.lock.Unlock()
b.mu.Lock()
defer b.mu.Unlock()
return os.Remove(b.filename(&id))
}
@ -353,8 +354,8 @@ func (b *multiBackend) Len() (int, error) {
}
func (b *multiBackend) close() error {
b.lock.Lock()
defer b.lock.Unlock()
b.mu.Lock()
defer b.mu.Unlock()
err := b.lockfile.Close()
if err == nil || errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrClosed) {
@ -364,9 +365,10 @@ func (b *multiBackend) close() error {
}
// NewMulti returns an instance of the multi-file store.
func NewMulti(runDir string) Store {
b := new(multiStore)
b.base = path.Join(runDir, "state")
b.backends = new(sync.Map)
return b
func NewMulti(msg container.Msg, runDir string) Store {
return &multiStore{
msg: msg,
base: path.Join(runDir, "state"),
backends: new(sync.Map),
}
}

View File

@ -1,9 +1,13 @@
package state_test
import (
"log"
"testing"
"hakurei.app/container"
"hakurei.app/internal/app/state"
)
func TestMulti(t *testing.T) { testStore(t, state.NewMulti(t.TempDir())) }
func TestMulti(t *testing.T) {
testStore(t, state.NewMulti(container.NewMsg(log.New(log.Writer(), "multi: ", 0)), t.TempDir()))
}

View File

@ -19,9 +19,9 @@ type Store interface {
// Cursor provided to f becomes invalid as soon as f returns.
Do(identity int, f func(c Cursor)) (ok bool, err error)
// List queries the store and returns a list of aids known to the store.
// Note that some or all returned aids might not have any active apps.
List() (aids []int, err error)
// List queries the store and returns a list of identities known to the store.
// Note that some or all returned identities might not have any active apps.
List() (identities []int, err error)
// Close releases any resources held by Store.
Close() error

View File

@ -16,10 +16,10 @@ import (
func testStore(t *testing.T, s state.Store) {
t.Run("list empty store", func(t *testing.T) {
if aids, err := s.List(); err != nil {
if identities, err := s.List(); err != nil {
t.Fatalf("List: error = %v", err)
} else if len(aids) != 0 {
t.Fatalf("List: aids = %#v", aids)
} else if len(identities) != 0 {
t.Fatalf("List: identities = %#v", identities)
}
})
@ -39,22 +39,22 @@ func testStore(t *testing.T, s state.Store) {
makeState(t, &tc[i].state, &tc[i].ct)
}
do := func(aid int, f func(c state.Cursor)) {
if ok, err := s.Do(aid, f); err != nil {
do := func(identity int, f func(c state.Cursor)) {
if ok, err := s.Do(identity, f); err != nil {
t.Fatalf("Do: ok = %v, error = %v", ok, err)
}
}
insert := func(i, aid int) {
do(aid, func(c state.Cursor) {
insert := func(i, identity int) {
do(identity, func(c state.Cursor) {
if err := c.Save(&tc[i].state, &tc[i].ct); err != nil {
t.Fatalf("Save(&tc[%v]): error = %v", i, err)
}
})
}
check := func(i, aid int) {
do(aid, func(c state.Cursor) {
check := func(i, identity int) {
do(identity, func(c state.Cursor) {
if entries, err := c.Load(); err != nil {
t.Fatalf("Load: error = %v", err)
} else if got, ok := entries[tc[i].state.ID]; !ok {
@ -81,7 +81,7 @@ func testStore(t *testing.T, s state.Store) {
insert(insertEntryNoCheck, 0)
})
t.Run("insert entry different aid", func(t *testing.T) {
t.Run("insert entry different identity", func(t *testing.T) {
insert(insertEntryOtherApp, 1)
check(insertEntryOtherApp, 1)
})
@ -90,14 +90,14 @@ func testStore(t *testing.T, s state.Store) {
check(insertEntryNoCheck, 0)
})
t.Run("list aids", func(t *testing.T) {
if aids, err := s.List(); err != nil {
t.Run("list identities", func(t *testing.T) {
if identities, err := s.List(); err != nil {
t.Fatalf("List: error = %v", err)
} else {
slices.Sort(aids)
slices.Sort(identities)
want := []int{0, 1}
if !slices.Equal(aids, want) {
t.Fatalf("List() = %#v, want %#v", aids, want)
if !slices.Equal(identities, want) {
t.Fatalf("List() = %#v, want %#v", identities, want)
}
}
})
@ -110,7 +110,7 @@ func testStore(t *testing.T, s state.Store) {
}
})
t.Run("clear aid 1", func(t *testing.T) {
t.Run("clear identity 1", func(t *testing.T) {
do(1, func(c state.Cursor) {
if err := c.Destroy(tc[insertEntryOtherApp].state.ID); err != nil {
t.Fatalf("Destroy: error = %v", err)

View File

@ -1,9 +0,0 @@
package internal
import (
"os"
"hakurei.app/internal/hlog"
)
func Exit(code int) { hlog.BeforeExit(); os.Exit(code) }

View File

@ -1,34 +0,0 @@
// Package hlog provides various functions for output messages.
package hlog
import (
"log"
"os"
"hakurei.app/container"
)
var o = &container.Suspendable{Downstream: os.Stderr}
// Prepare configures the system logger for [Suspend] and [Resume] to take effect.
func Prepare(prefix string) { log.SetPrefix(prefix + ": "); log.SetFlags(0); log.SetOutput(o) }
func Suspend() bool { return o.Suspend() }
func Resume() bool {
resumed, dropped, _, err := o.Resume()
if err != nil {
// probably going to result in an error as well,
// so this call is as good as unreachable
log.Printf("cannot dump buffer on resume: %v", err)
}
if resumed && dropped > 0 {
log.Fatalf("dropped %d bytes while output is suspended", dropped)
}
return resumed
}
func BeforeExit() {
if Resume() {
log.Printf("beforeExit reached on suspended output")
}
}

View File

@ -1,10 +0,0 @@
package hlog
type Output struct{}
func (Output) IsVerbose() bool { return Load() }
func (Output) Verbose(v ...any) { Verbose(v...) }
func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) }
func (Output) Suspend() { Suspend() }
func (Output) Resume() bool { return Resume() }
func (Output) BeforeExit() { BeforeExit() }

View File

@ -1,23 +0,0 @@
package hlog
import (
"log"
"sync/atomic"
)
var verbose = new(atomic.Bool)
func Load() bool { return verbose.Load() }
func Store(v bool) { verbose.Store(v) }
func Verbosef(format string, v ...any) {
if verbose.Load() {
log.Printf(format, v...)
}
}
func Verbose(v ...any) {
if verbose.Load() {
log.Println(v...)
}
}

View File

@ -1,13 +0,0 @@
package internal
import (
"hakurei.app/container"
"hakurei.app/internal/hlog"
"hakurei.app/system"
)
func InstallOutput(verbose bool) {
hlog.Store(verbose)
container.SetOutput(hlog.Output{})
system.SetOutput(hlog.Output{})
}

View File

@ -2,9 +2,8 @@ package internal
import (
"log"
"path"
"hakurei.app/internal/hlog"
"hakurei.app/container"
)
var (
@ -12,22 +11,23 @@ var (
hsu = compPoison
)
func MustHakureiPath() string {
if name, ok := checkPath(hmain); ok {
return name
}
hlog.BeforeExit()
log.Fatal("invalid hakurei path, this program is compiled incorrectly")
return compPoison // unreachable
}
// MustHakureiPath returns the absolute path to hakurei, configured at compile time.
func MustHakureiPath() *container.Absolute { return mustCheckPath(log.Fatal, "hakurei", hmain) }
func MustHsuPath() string {
if name, ok := checkPath(hsu); ok {
return name
}
hlog.BeforeExit()
log.Fatal("invalid hsu path, this program is compiled incorrectly")
return compPoison // unreachable
}
// MustHsuPath returns the absolute path to hakurei, configured at compile time.
func MustHsuPath() *container.Absolute { return mustCheckPath(log.Fatal, "hsu", hsu) }
func checkPath(p string) (string, bool) { return p, p != compPoison && p != "" && path.IsAbs(p) }
// mustCheckPath checks a pathname against compPoison, then [container.NewAbs], calling fatal if either step fails.
func mustCheckPath(fatal func(v ...any), name, pathname string) *container.Absolute {
if pathname != compPoison && pathname != "" {
if a, err := container.NewAbs(pathname); err != nil {
fatal(err.Error())
return nil // unreachable
} else {
return a
}
} else {
fatal("invalid " + name + " path, this program is compiled incorrectly")
return nil // unreachable
}
}

43
internal/path_test.go Normal file
View File

@ -0,0 +1,43 @@
package internal
import (
"reflect"
"testing"
"hakurei.app/container"
)
func TestMustCheckPath(t *testing.T) {
testCases := []struct {
name string
pathname string
wantFatal string
}{
{"poison", compPoison, "invalid test path, this program is compiled incorrectly"},
{"zero", "", "invalid test path, this program is compiled incorrectly"},
{"not absolute", "\x00", `path "\x00" is not absolute`},
{"success", "/proc/nonexistent", ""},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fatal := func(v ...any) { t.Fatal(append([]any{"invalid call to fatal:"}, v...)...) }
if tc.wantFatal != "" {
fatal = func(v ...any) {
if len(v) != 1 {
t.Errorf("mustCheckPath: fatal %#v", v)
} else if gotFatal, ok := v[0].(string); !ok {
t.Errorf("mustCheckPath: fatal = %#v", v[0])
} else if gotFatal != tc.wantFatal {
t.Errorf("mustCheckPath: fatal = %q, want %q", gotFatal, tc.wantFatal)
}
// do not simulate exit
}
}
if got := mustCheckPath(fatal, "test", tc.pathname); got != nil && !reflect.DeepEqual(got, container.MustAbs(tc.pathname)) {
t.Errorf("mustCheckPath: %q", got)
}
})
}
}

View File

@ -22,7 +22,7 @@ var (
msgStaticGlibc = []byte("not a dynamic executable")
)
func Exec(ctx context.Context, p string) ([]*Entry, error) {
func Exec(ctx context.Context, msg container.Msg, p string) ([]*Entry, error) {
c, cancel := context.WithTimeout(ctx, lddTimeout)
defer cancel()
@ -33,7 +33,7 @@ func Exec(ctx context.Context, p string) ([]*Entry, error) {
return nil, err
}
z := container.NewCommand(c, toolPath, lddName, p)
z := container.NewCommand(c, msg, toolPath, lddName, p)
z.Hostname = "hakurei-" + lddName
z.SeccompFlags |= seccomp.AllowMultiarch
z.SeccompPresets |= seccomp.PresetStrict

View File

@ -6,6 +6,7 @@ import (
"os"
"slices"
"hakurei.app/hst"
"hakurei.app/system/acl"
)
@ -16,37 +17,37 @@ func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I {
}
// UpdatePermType maintains [acl.Perms] on a file until its [Enablement] is no longer satisfied.
func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I {
func (sys *I) UpdatePermType(et hst.Enablement, path string, perms ...acl.Perm) *I {
sys.ops = append(sys.ops, &aclUpdateOp{et, path, perms})
return sys
}
// aclUpdateOp implements [I.UpdatePermType].
type aclUpdateOp struct {
et Enablement
et hst.Enablement
path string
perms acl.Perms
}
func (a *aclUpdateOp) Type() Enablement { return a.et }
func (a *aclUpdateOp) Type() hst.Enablement { return a.et }
func (a *aclUpdateOp) apply(sys *I) error {
sys.verbose("applying ACL", a)
sys.msg.Verbose("applying ACL", a)
return newOpError("acl", sys.aclUpdate(a.path, sys.uid, a.perms...), false)
}
func (a *aclUpdateOp) revert(sys *I, ec *Criteria) error {
if ec.hasType(a.Type()) {
sys.verbose("stripping ACL", a)
sys.msg.Verbose("stripping ACL", a)
err := sys.aclUpdate(a.path, sys.uid)
if errors.Is(err, os.ErrNotExist) {
// the ACL is effectively stripped if the file no longer exists
sys.verbosef("target of ACL %s no longer exists", a)
sys.msg.Verbosef("target of ACL %s no longer exists", a)
err = nil
}
return newOpError("acl", err, true)
} else {
sys.verbose("skipping ACL", a)
sys.msg.Verbose("skipping ACL", a)
return nil
}
}

View File

@ -6,6 +6,7 @@ import (
"testing"
"hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/system/acl"
)
@ -94,9 +95,9 @@ func TestACLUpdateOp(t *testing.T) {
}, stub.Expect{}},
{"wayland", 0xdeadbeef, func(_ *testing.T, sys *I) {
sys.UpdatePermType(EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute)
sys.UpdatePermType(hst.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute)
}, []Op{
&aclUpdateOp{EWayland, "/run/user/1971/wayland-0", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
&aclUpdateOp{hst.EWayland, "/run/user/1971/wayland-0", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
}, stub.Expect{}},
})
@ -106,34 +107,34 @@ func TestACLUpdateOp(t *testing.T) {
{"et differs",
&aclUpdateOp{
EWayland, "/run/user/1971/wayland-0",
hst.EWayland, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, &aclUpdateOp{
EX11, "/run/user/1971/wayland-0",
hst.EX11, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, false},
{"path differs", &aclUpdateOp{
EWayland, "/run/user/1971/wayland-0",
hst.EWayland, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, &aclUpdateOp{
EWayland, "/run/user/1971/wayland-1",
hst.EWayland, "/run/user/1971/wayland-1",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, false},
{"perms differs", &aclUpdateOp{
EWayland, "/run/user/1971/wayland-0",
hst.EWayland, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, &aclUpdateOp{
EWayland, "/run/user/1971/wayland-0",
hst.EWayland, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write},
}, false},
{"equals", &aclUpdateOp{
EWayland, "/run/user/1971/wayland-0",
hst.EWayland, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, &aclUpdateOp{
EWayland, "/run/user/1971/wayland-0",
hst.EWayland, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, true},
})
@ -160,23 +161,23 @@ func TestACLUpdateOp(t *testing.T) {
`--x type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2"`},
{"wayland",
&aclUpdateOp{EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland", []acl.Perm{acl.Read, acl.Write}},
EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland",
&aclUpdateOp{hst.EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland", []acl.Perm{acl.Read, acl.Write}},
hst.EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland",
`rw- type: wayland path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland"`},
{"x11",
&aclUpdateOp{EX11, "/tmp/.X11-unix/X0", []acl.Perm{acl.Read, acl.Execute}},
EX11, "/tmp/.X11-unix/X0",
&aclUpdateOp{hst.EX11, "/tmp/.X11-unix/X0", []acl.Perm{acl.Read, acl.Execute}},
hst.EX11, "/tmp/.X11-unix/X0",
`r-x type: x11 path: "/tmp/.X11-unix/X0"`},
{"dbus",
&aclUpdateOp{EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus", []acl.Perm{acl.Write, acl.Execute}},
EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus",
&aclUpdateOp{hst.EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus", []acl.Perm{acl.Write, acl.Execute}},
hst.EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus",
`-wx type: dbus path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus"`},
{"pulseaudio",
&aclUpdateOp{EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse",
&aclUpdateOp{hst.EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
hst.EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse",
`rwx type: pulseaudio path: "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse"`},
})
}

View File

@ -12,6 +12,7 @@ import (
"syscall"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/system/dbus"
)
@ -54,14 +55,14 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
return nil, newOpErrorMessage("dbus", err,
fmt.Sprintf("cannot finalise message bus proxy: %v", err), false)
} else {
if sys.isVerbose() {
sys.verbose("session bus proxy:", session.Args(sessionBus))
if sys.msg.IsVerbose() {
sys.msg.Verbose("session bus proxy:", session.Args(sessionBus))
if system != nil {
sys.verbose("system bus proxy:", system.Args(systemBus))
sys.msg.Verbose("system bus proxy:", system.Args(systemBus))
}
// this calls the argsWt String method
sys.verbose("message bus proxy final args:", final.WriterTo)
sys.msg.Verbose("message bus proxy final args:", final.WriterTo)
}
d.final = final
@ -81,31 +82,31 @@ type dbusProxyOp struct {
system bool
}
func (d *dbusProxyOp) Type() Enablement { return Process }
func (d *dbusProxyOp) Type() hst.Enablement { return Process }
func (d *dbusProxyOp) apply(sys *I) error {
sys.verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0])
sys.msg.Verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0])
if d.system {
sys.verbosef("system bus proxy on %q for upstream %q", d.final.System[1], d.final.System[0])
sys.msg.Verbosef("system bus proxy on %q for upstream %q", d.final.System[1], d.final.System[0])
}
d.proxy = dbus.New(sys.ctx, d.final, d.out)
d.proxy = dbus.New(sys.ctx, sys.msg, d.final, d.out)
if err := sys.dbusProxyStart(d.proxy); err != nil {
d.out.Dump()
return newOpErrorMessage("dbus", err,
fmt.Sprintf("cannot start message bus proxy: %v", err), false)
}
sys.verbose("starting message bus proxy", d.proxy)
sys.msg.Verbose("starting message bus proxy", d.proxy)
return nil
}
func (d *dbusProxyOp) revert(sys *I, _ *Criteria) error {
// criteria ignored here since dbus is always process-scoped
sys.verbose("terminating message bus proxy")
sys.msg.Verbose("terminating message bus proxy")
sys.dbusProxyClose(d.proxy)
exitMessage := "message bus proxy exit"
defer func() { sys.verbose(exitMessage) }()
defer func() { sys.msg.Verbose(exitMessage) }()
err := sys.dbusProxyWait(d.proxy)
if errors.Is(err, context.Canceled) {

View File

@ -11,6 +11,7 @@ import (
"testing"
"time"
"hakurei.app/container"
"hakurei.app/helper"
"hakurei.app/system/dbus"
)
@ -92,9 +93,9 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
t.Run("invalid start", func(t *testing.T) {
if !useSandbox {
p = dbus.NewDirect(t.Context(), nil, nil)
p = dbus.NewDirect(t.Context(), container.NewMsg(nil), nil, nil)
} else {
p = dbus.New(t.Context(), nil, nil)
p = dbus.New(t.Context(), container.NewMsg(nil), nil, nil)
}
if err := p.Start(); !errors.Is(err, syscall.ENOTRECOVERABLE) {
@ -127,9 +128,9 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
defer cancel()
output := new(strings.Builder)
if !useSandbox {
p = dbus.NewDirect(ctx, final, output)
p = dbus.NewDirect(ctx, container.NewMsg(nil), final, output)
} else {
p = dbus.New(ctx, final, output)
p = dbus.New(ctx, container.NewMsg(nil), final, output)
}
t.Run("invalid wait", func(t *testing.T) {

View File

@ -3,11 +3,13 @@ package dbus
import (
"context"
"io"
"hakurei.app/container"
)
// NewDirect returns a new instance of [Proxy] with its sandbox disabled.
func NewDirect(ctx context.Context, final *Final, output io.Writer) *Proxy {
p := New(ctx, final, output)
func NewDirect(ctx context.Context, msg container.Msg, final *Final, output io.Writer) *Proxy {
p := New(ctx, msg, final, output)
p.useSandbox = false
return p
}

View File

@ -52,14 +52,14 @@ func (p *Proxy) Start() error {
}
var libPaths []*container.Absolute
if entries, err := ldd.Exec(ctx, toolPath.String()); err != nil {
if entries, err := ldd.Exec(ctx, p.msg, toolPath.String()); err != nil {
return err
} else {
libPaths = ldd.Path(entries)
}
p.helper = helper.New(
ctx, toolPath, "xdg-dbus-proxy",
ctx, p.msg, toolPath, "xdg-dbus-proxy",
p.final, true,
argF, func(z *container.Container) {
z.SeccompFlags |= seccomp.AllowMultiarch

View File

@ -6,12 +6,10 @@ import (
"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)
container.TryArgv0(nil)
helper.InternalHelperStub()
os.Exit(m.Run())
}

View File

@ -7,6 +7,7 @@ import (
"sync"
"syscall"
"hakurei.app/container"
"hakurei.app/helper"
)
@ -27,6 +28,7 @@ func (e *BadInterfaceError) Error() string {
type Proxy struct {
helper helper.Helper
ctx context.Context
msg container.Msg
cancel context.CancelCauseFunc
cause func() error
@ -107,6 +109,6 @@ func Finalise(sessionBus, systemBus ProxyPair, session, system *Config) (final *
}
// New returns a new instance of [Proxy].
func New(ctx context.Context, final *Final, output io.Writer) *Proxy {
return &Proxy{name: ProxyName, ctx: ctx, final: final, output: output, useSandbox: true}
func New(ctx context.Context, msg container.Msg, final *Final, output io.Writer) *Proxy {
return &Proxy{name: ProxyName, ctx: ctx, msg: msg, final: final, output: output, useSandbox: true}
}

View File

@ -57,10 +57,6 @@ type syscallDispatcher interface {
dbusProxyClose(proxy *dbus.Proxy)
// dbusProxyWait provides the Wait method of [dbus.Proxy].
dbusProxyWait(proxy *dbus.Proxy) error
isVerbose() bool
verbose(v ...any)
verbosef(format string, v ...any)
}
// direct implements syscallDispatcher on the current kernel.
@ -96,7 +92,3 @@ func (k direct) dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, syst
func (k direct) dbusProxyStart(proxy *dbus.Proxy) error { return proxy.Start() }
func (k direct) dbusProxyClose(proxy *dbus.Proxy) { proxy.Close() }
func (k direct) dbusProxyWait(proxy *dbus.Proxy) error { return proxy.Wait() }
func (k direct) isVerbose() bool { return msg.IsVerbose() }
func (direct) verbose(v ...any) { msg.Verbose(v...) }
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }

View File

@ -3,6 +3,7 @@ package system
import (
"io"
"io/fs"
"log"
"os"
"reflect"
"slices"
@ -11,6 +12,7 @@ import (
"unsafe"
"hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/system/acl"
"hakurei.app/system/dbus"
"hakurei.app/system/internal/xcb"
@ -25,7 +27,7 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
type opBehaviourTestCase struct {
name string
uid int
ec Enablement
ec hst.Enablement
op Op
apply []stub.Call
@ -141,7 +143,7 @@ type opMetaTestCase struct {
name string
op Op
wantType Enablement
wantType hst.Enablement
wantPath string
wantString string
}
@ -214,10 +216,10 @@ func (r *readerOsFile) Close() error {
// InternalNew initialises [I] with a stub syscallDispatcher.
func InternalNew(t *testing.T, want stub.Expect, uid int) (*I, *stub.Stub[syscallDispatcher]) {
k := stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} }, want)
sys := New(t.Context(), uid)
sys.syscallDispatcher = &kstub{k}
return sys, k
k := &kstub{stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} }, want)}
sys := New(t.Context(), k, uid)
sys.syscallDispatcher = k
return sys, k.Stub
}
type kstub struct{ *stub.Stub[syscallDispatcher] }
@ -357,12 +359,23 @@ func (k *kstub) dbusProxySCW(expect *stub.Call, proxy *dbus.Proxy) error {
return expect.Err
}
func (k *kstub) isVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
func (k *kstub) SwapVerbose(verbose bool) bool {
k.Helper()
expect := k.Expects("swapVerbose")
if expect.Error(
stub.CheckArg(k.Stub, "verbose", verbose, 0)) != nil {
k.FailNow()
}
return expect.Ret.(bool)
}
// ignoreValue marks a value to be ignored by the test suite.
type ignoreValue struct{}
func (k *kstub) verbose(v ...any) {
func (k *kstub) Verbose(v ...any) {
k.Helper()
expect := k.Expects("verbose")
@ -381,7 +394,7 @@ func (k *kstub) verbose(v ...any) {
}
}
func (k *kstub) verbosef(format string, v ...any) {
func (k *kstub) Verbosef(format string, v ...any) {
k.Helper()
if k.Expects("verbosef").Error(
stub.CheckArg(k.Stub, "format", format, 0),
@ -389,3 +402,7 @@ func (k *kstub) verbosef(format string, v ...any) {
k.FailNow()
}
}
func (k *kstub) Suspend() bool { k.Helper(); return k.Expects("suspend").Ret.(bool) }
func (k *kstub) Resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
func (k *kstub) BeforeExit() { k.Helper(); k.Expects("beforeExit") }

View File

@ -1,47 +0,0 @@
package system
import (
"fmt"
"strings"
)
// Enablement represents an optional host service to export to the target user.
type Enablement byte
const (
EWayland Enablement = 1 << iota
EX11
EDBus
EPulse
EM
)
func (e Enablement) String() string {
switch e {
case 0:
return "(no enablements)"
case EWayland:
return "wayland"
case EX11:
return "x11"
case EDBus:
return "dbus"
case EPulse:
return "pulseaudio"
default:
buf := new(strings.Builder)
buf.Grow(32)
for i := Enablement(1); i < EM; i <<= 1 {
if e&i != 0 {
buf.WriteString(", " + i.String())
}
}
if buf.Len() == 0 {
return fmt.Sprintf("e%x", byte(e))
}
return strings.TrimPrefix(buf.String(), ", ")
}
}

View File

@ -1,43 +0,0 @@
package system_test
import (
"testing"
"hakurei.app/system"
)
func TestEnablementString(t *testing.T) {
testCases := []struct {
flags system.Enablement
want string
}{
{0, "(no enablements)"},
{system.EWayland, "wayland"},
{system.EX11, "x11"},
{system.EDBus, "dbus"},
{system.EPulse, "pulseaudio"},
{system.EWayland | system.EX11, "wayland, x11"},
{system.EWayland | system.EDBus, "wayland, dbus"},
{system.EWayland | system.EPulse, "wayland, pulseaudio"},
{system.EX11 | system.EDBus, "x11, dbus"},
{system.EX11 | system.EPulse, "x11, pulseaudio"},
{system.EDBus | system.EPulse, "dbus, pulseaudio"},
{system.EWayland | system.EX11 | system.EDBus, "wayland, x11, dbus"},
{system.EWayland | system.EX11 | system.EPulse, "wayland, x11, pulseaudio"},
{system.EWayland | system.EDBus | system.EPulse, "wayland, dbus, pulseaudio"},
{system.EX11 | system.EDBus | system.EPulse, "x11, dbus, pulseaudio"},
{system.EWayland | system.EX11 | system.EDBus | system.EPulse, "wayland, x11, dbus, pulseaudio"},
{1 << 5, "e20"},
{1 << 6, "e40"},
{1 << 7, "e80"},
}
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
if got := tc.flags.String(); got != tc.want {
t.Errorf("String: %q, want %q", got, tc.want)
}
})
}
}

View File

@ -2,36 +2,38 @@ package system
import (
"fmt"
"hakurei.app/hst"
)
// Link calls LinkFileType with the [Process] criteria.
func (sys *I) Link(oldname, newname string) *I { return sys.LinkFileType(Process, oldname, newname) }
// LinkFileType maintains a hardlink until its [Enablement] is no longer satisfied.
func (sys *I) LinkFileType(et Enablement, oldname, newname string) *I {
func (sys *I) LinkFileType(et hst.Enablement, oldname, newname string) *I {
sys.ops = append(sys.ops, &hardlinkOp{et, newname, oldname})
return sys
}
// hardlinkOp implements [I.LinkFileType].
type hardlinkOp struct {
et Enablement
et hst.Enablement
dst, src string
}
func (l *hardlinkOp) Type() Enablement { return l.et }
func (l *hardlinkOp) Type() hst.Enablement { return l.et }
func (l *hardlinkOp) apply(sys *I) error {
sys.verbose("linking", l)
sys.msg.Verbose("linking", l)
return newOpError("hardlink", sys.link(l.src, l.dst), false)
}
func (l *hardlinkOp) revert(sys *I, ec *Criteria) error {
if ec.hasType(l.Type()) {
sys.verbosef("removing hard link %q", l.dst)
sys.msg.Verbosef("removing hard link %q", l.dst)
return newOpError("hardlink", sys.remove(l.dst), true)
} else {
sys.verbosef("skipping hard link %q", l.dst)
sys.msg.Verbosef("skipping hard link %q", l.dst)
return nil
}
}

View File

@ -4,32 +4,33 @@ import (
"testing"
"hakurei.app/container/stub"
"hakurei.app/hst"
)
func TestHardlinkOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"link", 0xdeadbeef, 0xff, &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
{"link", 0xdeadbeef, 0xff, &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, stub.UniqueError(1)),
}, &OpError{Op: "hardlink", Err: stub.UniqueError(1)}, nil, nil},
{"remove", 0xdeadbeef, 0xff, &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
{"remove", 0xdeadbeef, 0xff, &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, nil),
}, nil, []stub.Call{
call("verbosef", stub.ExpectArgs{"removing hard link %q", []any{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}}, nil, nil),
call("remove", stub.ExpectArgs{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, stub.UniqueError(0)),
}, &OpError{Op: "hardlink", Err: stub.UniqueError(0), Revert: true}},
{"success skip", 0xdeadbeef, EWayland | EX11, &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
{"success skip", 0xdeadbeef, hst.EWayland | hst.EX11, &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, nil),
}, nil, []stub.Call{
call("verbosef", stub.ExpectArgs{"skipping hard link %q", []any{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}}, nil, nil),
}, nil},
{"success", 0xdeadbeef, 0xff, &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
{"success", 0xdeadbeef, 0xff, &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil),
call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, nil),
}, nil, []stub.Call{
call("verbosef", stub.ExpectArgs{"removing hard link %q", []any{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}}, nil, nil),

View File

@ -4,6 +4,8 @@ import (
"errors"
"fmt"
"os"
"hakurei.app/hst"
)
// Ensure ensures the existence of a directory.
@ -13,23 +15,23 @@ func (sys *I) Ensure(name string, perm os.FileMode) *I {
}
// Ephemeral ensures the existence of a directory until its [Enablement] is no longer satisfied.
func (sys *I) Ephemeral(et Enablement, name string, perm os.FileMode) *I {
func (sys *I) Ephemeral(et hst.Enablement, name string, perm os.FileMode) *I {
sys.ops = append(sys.ops, &mkdirOp{et, name, perm, true})
return sys
}
// mkdirOp implements [I.Ensure] and [I.Ephemeral].
type mkdirOp struct {
et Enablement
et hst.Enablement
path string
perm os.FileMode
ephemeral bool
}
func (m *mkdirOp) Type() Enablement { return m.et }
func (m *mkdirOp) Type() hst.Enablement { return m.et }
func (m *mkdirOp) apply(sys *I) error {
sys.verbose("ensuring directory", m)
sys.msg.Verbose("ensuring directory", m)
if err := sys.mkdir(m.path, m.perm); err != nil {
if !errors.Is(err, os.ErrExist) {
@ -49,10 +51,10 @@ func (m *mkdirOp) revert(sys *I, ec *Criteria) error {
}
if ec.hasType(m.Type()) {
sys.verbose("destroying ephemeral directory", m)
sys.msg.Verbose("destroying ephemeral directory", m)
return newOpError("mkdir", sys.remove(m.path), true)
} else {
sys.verbose("skipping ephemeral directory", m)
sys.msg.Verbose("skipping ephemeral directory", m)
return nil
}
}

View File

@ -8,16 +8,6 @@ import (
"hakurei.app/container"
)
var msg container.Msg = new(container.DefaultMsg)
func SetOutput(v container.Msg) {
if v == nil {
msg = new(container.DefaultMsg)
} else {
msg = v
}
}
// OpError is returned by [I.Commit] and [I.Revert].
type OpError struct {
Op string

View File

@ -9,7 +9,6 @@ import (
"testing"
"hakurei.app/container"
"hakurei.app/internal/hlog"
)
func TestOpError(t *testing.T) {
@ -87,33 +86,6 @@ func TestOpError(t *testing.T) {
})
}
func TestSetOutput(t *testing.T) {
oldmsg := msg
t.Cleanup(func() { msg = oldmsg })
msg = nil
t.Run("nil", func(t *testing.T) {
SetOutput(nil)
if _, ok := msg.(*container.DefaultMsg); !ok {
t.Errorf("SetOutput: %#v", msg)
}
})
t.Run("hlog", func(t *testing.T) {
SetOutput(hlog.Output{})
if _, ok := msg.(hlog.Output); !ok {
t.Errorf("SetOutput: %#v", msg)
}
})
t.Run("reset", func(t *testing.T) {
SetOutput(nil)
if _, ok := msg.(*container.DefaultMsg); !ok {
t.Errorf("SetOutput: %#v", msg)
}
})
}
func TestPrintJoinedError(t *testing.T) {
testCases := []struct {
name string

View File

@ -5,11 +5,14 @@ import (
"context"
"errors"
"strings"
"hakurei.app/container"
"hakurei.app/hst"
)
const (
// User type is reverted at final instance exit.
User = EM << iota
User = hst.EM << iota
// Process type is unconditionally reverted on exit.
Process
@ -17,21 +20,21 @@ const (
)
// Criteria specifies types of Op to revert.
type Criteria Enablement
type Criteria hst.Enablement
func (ec *Criteria) hasType(t Enablement) bool {
func (ec *Criteria) hasType(t hst.Enablement) bool {
// nil criteria: revert everything except User
if ec == nil {
return t != User
}
return Enablement(*ec)&t != 0
return hst.Enablement(*ec)&t != 0
}
// Op is a reversible system operation.
type Op interface {
// Type returns [Op]'s enablement type, for matching a revert criteria.
Type() Enablement
Type() hst.Enablement
apply(sys *I) error
revert(sys *I, ec *Criteria) error
@ -42,7 +45,7 @@ type Op interface {
}
// TypeString extends [Enablement.String] to support [User] and [Process].
func TypeString(e Enablement) string {
func TypeString(e hst.Enablement) string {
switch e {
case User:
return "user"
@ -65,11 +68,11 @@ func TypeString(e Enablement) string {
}
// New returns the address of a new [I] targeting uid.
func New(ctx context.Context, uid int) (sys *I) {
if ctx == nil || uid < 0 {
func New(ctx context.Context, msg container.Msg, uid int) (sys *I) {
if ctx == nil || msg == nil || uid < 0 {
panic("invalid call to New")
}
return &I{ctx: ctx, uid: uid, syscallDispatcher: direct{}}
return &I{ctx: ctx, msg: msg, uid: uid, syscallDispatcher: direct{}}
}
// An I provides deferred operating system interaction. [I] must not be copied.
@ -86,6 +89,7 @@ type I struct {
// the behaviour of Revert is only defined for up to one call
reverted bool
msg container.Msg
syscallDispatcher
}
@ -114,14 +118,14 @@ func (sys *I) Commit() error {
}
sys.committed = true
sp := New(sys.ctx, sys.uid)
sp := New(sys.ctx, sys.msg, sys.uid)
sp.syscallDispatcher = sys.syscallDispatcher
sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits
defer func() {
// sp is set to nil when all ops are applied
if sp != nil {
// rollback partial commit
sys.verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops))
sys.msg.Verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops))
if err := sp.Revert(nil); err != nil {
printJoinedError(sys.println, "cannot revert partial commit:", err)
}

View File

@ -8,19 +8,21 @@ import (
"strconv"
"testing"
"hakurei.app/container"
"hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/system/internal/xcb"
)
func TestCriteria(t *testing.T) {
testCases := []struct {
name string
ec, t Enablement
ec, t hst.Enablement
want bool
}{
{"nil", 0xff, EWayland, true},
{"nil", 0xff, hst.EWayland, true},
{"nil user", 0xff, User, false},
{"all", EWayland | EX11 | EDBus | EPulse | User | Process, Process, true},
{"all", hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse | User | Process, Process, true},
}
for _, tc := range testCases {
@ -39,18 +41,18 @@ func TestCriteria(t *testing.T) {
func TestTypeString(t *testing.T) {
testCases := []struct {
e Enablement
e hst.Enablement
want string
}{
{EWayland, EWayland.String()},
{EX11, EX11.String()},
{EDBus, EDBus.String()},
{EPulse, EPulse.String()},
{hst.EWayland, hst.EWayland.String()},
{hst.EX11, hst.EX11.String()},
{hst.EDBus, hst.EDBus.String()},
{hst.EPulse, hst.EPulse.String()},
{User, "user"},
{Process, "process"},
{User | Process, "user, process"},
{EWayland | User | Process, "wayland, user, process"},
{EX11 | Process, "x11, process"},
{hst.EWayland | User | Process, "wayland, user, process"},
{hst.EX11 | Process, "x11, process"},
}
for _, tc := range testCases {
@ -71,7 +73,17 @@ func TestNew(t *testing.T) {
t.Errorf("recover: %v, want %v", r, want)
}
}()
New(nil, 0)
New(nil, container.NewMsg(nil), 0)
})
t.Run("msg", func(t *testing.T) {
defer func() {
want := "invalid call to New"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
New(t.Context(), nil, 0)
})
t.Run("uid", func(t *testing.T) {
@ -81,11 +93,11 @@ func TestNew(t *testing.T) {
t.Errorf("recover: %v, want %v", r, want)
}
}()
New(t.Context(), -1)
New(t.Context(), container.NewMsg(nil), -1)
})
})
sys := New(t.Context(), 0xdeadbeef)
sys := New(t.Context(), container.NewMsg(nil), 0xdeadbeef)
if sys.ctx == nil {
t.Error("New: ctx = nil")
}
@ -102,51 +114,51 @@ func TestEqual(t *testing.T) {
want bool
}{
{"simple UID",
New(t.Context(), 150),
New(t.Context(), 150),
New(t.Context(), container.NewMsg(nil), 150),
New(t.Context(), container.NewMsg(nil), 150),
true},
{"simple UID differ",
New(t.Context(), 150),
New(t.Context(), 151),
New(t.Context(), container.NewMsg(nil), 150),
New(t.Context(), container.NewMsg(nil), 151),
false},
{"simple UID nil",
New(t.Context(), 150),
New(t.Context(), container.NewMsg(nil), 150),
nil,
false},
{"op length mismatch",
New(t.Context(), 150).
New(t.Context(), container.NewMsg(nil), 150).
ChangeHosts("chronos"),
New(t.Context(), 150).
New(t.Context(), container.NewMsg(nil), 150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false},
{"op value mismatch",
New(t.Context(), 150).
New(t.Context(), container.NewMsg(nil), 150).
ChangeHosts("chronos").
Ensure("/run", 0644),
New(t.Context(), 150).
New(t.Context(), container.NewMsg(nil), 150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false},
{"op type mismatch",
New(t.Context(), 150).
New(t.Context(), container.NewMsg(nil), 150).
ChangeHosts("chronos").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 0, 256),
New(t.Context(), 150).
New(t.Context(), container.NewMsg(nil), 150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false},
{"op equals",
New(t.Context(), 150).
New(t.Context(), container.NewMsg(nil), 150).
ChangeHosts("chronos").
Ensure("/run", 0755),
New(t.Context(), 150).
New(t.Context(), container.NewMsg(nil), 150).
ChangeHosts("chronos").
Ensure("/run", 0755),
true},
@ -165,7 +177,7 @@ func TestCommitRevert(t *testing.T) {
testCases := []struct {
name string
f func(sys *I)
ec Enablement
ec hst.Enablement
commit []stub.Call
wantErrCommit error

View File

@ -7,6 +7,8 @@ import (
"io"
"os"
"syscall"
"hakurei.app/hst"
)
// CopyFile reads up to n bytes from src and writes the resulting byte slice to payloadP.
@ -26,7 +28,7 @@ type tmpfileOp struct {
buf *bytes.Buffer
}
func (t *tmpfileOp) Type() Enablement { return Process }
func (t *tmpfileOp) Type() hst.Enablement { return Process }
func (t *tmpfileOp) apply(sys *I) error {
if t.payload == nil {
@ -34,7 +36,7 @@ func (t *tmpfileOp) apply(sys *I) error {
return errors.New("invalid payload")
}
sys.verbose("copying", t)
sys.msg.Verbose("copying", t)
if b, err := sys.stat(t.src); err != nil {
return newOpError("tmpfile", err, false)
@ -58,7 +60,7 @@ func (t *tmpfileOp) apply(sys *I) error {
_ = r.Close()
return newOpError("tmpfile", err, false)
}
sys.verbosef("copied %d bytes from %q", n, t.src)
sys.msg.Verbosef("copied %d bytes from %q", n, t.src)
}
if err := r.Close(); err != nil {
return newOpError("tmpfile", err, false)

View File

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"hakurei.app/hst"
"hakurei.app/system/acl"
"hakurei.app/system/wayland"
)
@ -32,7 +33,7 @@ type waylandOp struct {
conn waylandConn
}
func (w *waylandOp) Type() Enablement { return Process }
func (w *waylandOp) Type() hst.Enablement { return Process }
func (w *waylandOp) apply(sys *I) error {
if w.sync == nil {
@ -43,14 +44,14 @@ func (w *waylandOp) apply(sys *I) error {
if err := w.conn.Attach(w.src); err != nil {
return newOpError("wayland", err, false)
} else {
sys.verbosef("wayland attached on %q", w.src)
sys.msg.Verbosef("wayland attached on %q", w.src)
}
if sp, err := w.conn.Bind(w.dst, w.appID, w.instanceID); err != nil {
return newOpError("wayland", err, false)
} else {
*w.sync = sp
sys.verbosef("wayland listening on %q", w.dst)
sys.msg.Verbosef("wayland listening on %q", w.dst)
if err = sys.chmod(w.dst, 0); err != nil {
return newOpError("wayland", err, false)
}
@ -59,12 +60,12 @@ func (w *waylandOp) apply(sys *I) error {
}
func (w *waylandOp) revert(sys *I, _ *Criteria) error {
sys.verbosef("removing wayland socket on %q", w.dst)
sys.msg.Verbosef("removing wayland socket on %q", w.dst)
if err := sys.remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) {
return newOpError("wayland", err, true)
}
sys.verbosef("detaching from wayland on %q", w.src)
sys.msg.Verbosef("detaching from wayland on %q", w.src)
return newOpError("wayland", w.conn.Close(), true)
}

View File

@ -1,6 +1,7 @@
package system
import (
"hakurei.app/hst"
"hakurei.app/system/internal/xcb"
)
@ -13,21 +14,21 @@ func (sys *I) ChangeHosts(username string) *I {
// xhostOp implements [I.ChangeHosts].
type xhostOp string
func (x xhostOp) Type() Enablement { return EX11 }
func (x xhostOp) Type() hst.Enablement { return hst.EX11 }
func (x xhostOp) apply(sys *I) error {
sys.verbosef("inserting entry %s to X11", x)
sys.msg.Verbosef("inserting entry %s to X11", x)
return newOpError("xhost",
sys.xcbChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), false)
}
func (x xhostOp) revert(sys *I, ec *Criteria) error {
if ec.hasType(x.Type()) {
sys.verbosef("deleting entry %s from X11", x)
sys.msg.Verbosef("deleting entry %s from X11", x)
return newOpError("xhost",
sys.xcbChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), true)
} else {
sys.verbosef("skipping entry %s in X11", x)
sys.msg.Verbosef("skipping entry %s in X11", x)
return nil
}
}

View File

@ -4,17 +4,18 @@ import (
"testing"
"hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/system/internal/xcb"
)
func TestXHostOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"xcbChangeHosts revert", 0xbeef, EX11, xhostOp("chronos"), []stub.Call{
{"xcbChangeHosts revert", 0xbeef, hst.EX11, xhostOp("chronos"), []stub.Call{
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(1)),
}, &OpError{Op: "xhost", Err: stub.UniqueError(1)}, nil, nil},
{"xcbChangeHosts revert", 0xbeef, EX11, xhostOp("chronos"), []stub.Call{
{"xcbChangeHosts revert", 0xbeef, hst.EX11, xhostOp("chronos"), []stub.Call{
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil),
}, nil, []stub.Call{
@ -29,7 +30,7 @@ func TestXHostOp(t *testing.T) {
call("verbosef", stub.ExpectArgs{"skipping entry %s in X11", []any{xhostOp("chronos")}}, nil, nil),
}, nil},
{"success", 0xbeef, EX11, xhostOp("chronos"), []stub.Call{
{"success", 0xbeef, hst.EX11, xhostOp("chronos"), []stub.Call{
call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil),
call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil),
}, nil, []stub.Call{
@ -52,6 +53,6 @@ func TestXHostOp(t *testing.T) {
})
checkOpMeta(t, []opMetaTestCase{
{"xhost", xhostOp("chronos"), EX11, "/tmp/.X11-unix", "SI:localuser:chronos"},
{"xhost", xhostOp("chronos"), hst.EX11, "/tmp/.X11-unix", "SI:localuser:chronos"},
})
}