9 Commits

Author SHA1 Message Date
52e3324ef4 test/sandbox: ignore nondeterministic mount point
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Sandbox (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Hpkg (push) Successful in 43s
Test / Hakurei (push) Successful in 47s
Test / Flake checks (push) Successful in 1m30s
No idea what systemd is doing with this to cause its options to change.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 07:08:39 +09:00
f95e0a7568 hst/config: hold acl struct by value
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hpkg (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Sandbox (push) Successful in 1m22s
Test / Hakurei (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m37s
Doc comments are also reworded for clarity.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 07:02:14 +09:00
4c647add0d hst/container: pack boolean options
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Sandbox (race detector) (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m37s
The memory saving is relatively insignificant, however this increases serialisation efficiency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 06:39:00 +09:00
a341466942 hst: separate container config
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hpkg (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Flake checks (push) Successful in 1m31s
The booleans are getting packed into a single field. This requires non-insignificant amount of code for JSON serialisation to stay compatible.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 04:23:05 +09:00
e4ee8df83c internal/app/spdbus: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m16s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hpkg (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Hakurei (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m30s
This is not done very cleanly, however this op is pending removal for the in-process dbus proxy so not worth spending too much effort here. As long as it checks all paths it is good enough.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 01:51:01 +09:00
048c1957f1 helper/args: variadic check function
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m30s
Test / Hakurei (push) Successful in 2m21s
Test / Hpkg (push) Successful in 3m23s
Test / Sandbox (race detector) (push) Successful in 4m1s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m27s
This package turns out to be much less widely used than anticipated, and might be facing removal. This change makes test cases cleaner.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 01:48:56 +09:00
790d77075e system/dbus: remove builder state leak
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Sandbox (push) Successful in 1m23s
Test / Hakurei (push) Successful in 2m14s
Test / Flake checks (push) Successful in 1m26s
This enables external testing of system.I state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 01:33:44 +09:00
e5ff40e7d3 container: synchronise after notify
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m23s
This should eliminate intermittent failures in the forward test.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 19:17:19 +09:00
123d7fbfd5 container/seccomp: remove export pipe
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hpkg (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m32s
This was only useful when wrapping bwrap.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 18:51:35 +09:00
40 changed files with 1025 additions and 648 deletions

View File

@@ -147,11 +147,6 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
Enablements: hst.NewEnablements(et), Enablements: hst.NewEnablements(et),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Userns: true,
HostNet: true,
Tty: true,
HostAbstract: true,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
// autoroot, includes the home directory // autoroot, includes the home directory
{FilesystemConfig: &hst.FSBind{ {FilesystemConfig: &hst.FSBind{
@@ -167,6 +162,8 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
Path: progPath, Path: progPath,
Args: args, Args: args,
Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty,
}, },
} }

View File

@@ -87,19 +87,19 @@ func printShowInstance(
t.Printf(" Hostname:\t%s\n", params.Hostname) t.Printf(" Hostname:\t%s\n", params.Hostname)
} }
flags := make([]string, 0, 7) flags := make([]string, 0, 7)
writeFlag := func(name string, value bool) { writeFlag := func(name string, flag uintptr, force bool) {
if value { if params.Flags&flag != 0 || force {
flags = append(flags, name) flags = append(flags, name)
} }
} }
writeFlag("userns", params.Userns) writeFlag("userns", hst.FUserns, false)
writeFlag("devel", params.Devel) writeFlag("devel", hst.FDevel, false)
writeFlag("net", params.HostNet) writeFlag("net", hst.FHostNet, false)
writeFlag("abstract", params.HostAbstract) writeFlag("abstract", hst.FHostAbstract, false)
writeFlag("device", params.Device) writeFlag("device", hst.FDevice, false)
writeFlag("tty", params.Tty) writeFlag("tty", hst.FTty, false)
writeFlag("mapuid", params.MapRealUID) writeFlag("mapuid", hst.FMapRealUID, false)
writeFlag("directwl", config.DirectWayland) writeFlag("directwl", 0, config.DirectWayland)
if len(flags) == 0 { if len(flags) == 0 {
flags = append(flags, "none") flags = append(flags, "none")
} }
@@ -129,11 +129,8 @@ func printShowInstance(
} }
if len(config.ExtraPerms) > 0 { if len(config.ExtraPerms) > 0 {
t.Printf("Extra ACL\n") t.Printf("Extra ACL\n")
for _, p := range config.ExtraPerms { for i := range config.ExtraPerms {
if p == nil { t.Printf(" %s\n", config.ExtraPerms[i].String())
continue
}
t.Printf(" %s\n", p.String())
} }
t.Printf("\n") t.Printf("\n")
} }

View File

@@ -88,7 +88,7 @@ App
Flags: none Flags: none
`, false}, `, false},
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfigJSON, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `Error: container configuration missing path to home directory! {"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfigJSON, 1)}, ExtraPerms: make([]hst.ExtraPermConfig, 1)}, false, false, `Error: container configuration missing path to home directory!
App App
Identity: 0 Identity: 0
@@ -99,6 +99,7 @@ Filesystem
<invalid> <invalid>
Extra ACL Extra ACL
<invalid>
`, false}, `, false},
{"config pd dbus see", nil, &hst.Config{SessionBus: &hst.BusConfig{See: []string{"org.example.test"}}}, false, false, `Error: configuration missing container state! {"config pd dbus see", nil, &hst.Config{SessionBus: &hst.BusConfig{See: []string{"org.example.test"}}}, false, false, `Error: configuration missing container state!
@@ -252,20 +253,11 @@ App
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1, "wait_delay": -1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"device": true,
"filesystem": [ "filesystem": [
{ {
"type": "bind", "type": "bind",
@@ -331,7 +323,16 @@ App
"--disable-smooth-scrolling", "--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
] ],
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"map_real_uid": true,
"device": true
} }
}, },
"time": "1970-01-01T00:00:00.000000009Z" "time": "1970-01-01T00:00:00.000000009Z"
@@ -402,20 +403,11 @@ App
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1, "wait_delay": -1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"device": true,
"filesystem": [ "filesystem": [
{ {
"type": "bind", "type": "bind",
@@ -481,7 +473,16 @@ App
"--disable-smooth-scrolling", "--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
] ],
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"map_real_uid": true,
"device": true
} }
} }
`, true}, `, true},
@@ -612,20 +613,11 @@ func TestPrintPs(t *testing.T) {
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1, "wait_delay": -1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"device": true,
"filesystem": [ "filesystem": [
{ {
"type": "bind", "type": "bind",
@@ -691,7 +683,16 @@ func TestPrintPs(t *testing.T) {
"--disable-smooth-scrolling", "--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
] ],
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"map_real_uid": true,
"device": true
} }
}, },
"time": "1970-01-01T00:00:00.000000009Z" "time": "1970-01-01T00:00:00.000000009Z"

View File

@@ -77,14 +77,6 @@ func (app *appInfo) toHst(pathSet *appPathSet, pathname *check.Absolute, argv []
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name), Hostname: formatHostname(app.Name),
Devel: app.Devel,
Userns: app.Userns,
HostNet: app.HostNet,
HostAbstract: app.HostAbstract,
Device: app.Device,
Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID,
Multiarch: app.Multiarch,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}}, {FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}}, {FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
@@ -108,11 +100,36 @@ func (app *appInfo) toHst(pathSet *appPathSet, pathname *check.Absolute, argv []
Path: pathname, Path: pathname,
Args: argv, Args: argv,
}, },
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
}, },
} }
if app.Devel {
config.Container.Flags |= hst.FDevel
}
if app.Userns {
config.Container.Flags |= hst.FUserns
}
if app.HostNet {
config.Container.Flags |= hst.FHostNet
}
if app.HostAbstract {
config.Container.Flags |= hst.FHostAbstract
}
if app.Device {
config.Container.Flags |= hst.FDevice
}
if app.Tty || flagDropShell {
config.Container.Flags |= hst.FTty
}
if app.MapRealUID {
config.Container.Flags |= hst.FMapRealUID
}
if app.Multiarch {
config.Container.Flags |= hst.FMultiarch
}
return config return config
} }

View File

@@ -17,10 +17,18 @@ func withNixDaemon(
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config, action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(), app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) { ) {
flags := hst.FMultiarch | hst.FUserns // nix sandbox requires userns
if net {
flags |= hst.FHostNet
}
if dropShell {
flags |= hst.FTty
}
mustRunAppDropShell(ctx, msg, updateConfig(&hst.Config{ mustRunAppDropShell(ctx, msg, updateConfig(&hst.Config{
ID: app.ID, ID: app.ID,
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
}, },
@@ -29,10 +37,7 @@ func withNixDaemon(
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
HostNet: net,
Multiarch: true,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}}, {FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}}, {FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
@@ -58,6 +63,8 @@ func withNixDaemon(
// terminate nix-daemon // terminate nix-daemon
" && pkill nix-daemon", " && pkill nix-daemon",
}, },
Flags: flags,
}, },
}), dropShell, beforeFail) }), dropShell, beforeFail)
} }
@@ -66,11 +73,17 @@ func withCacheDir(
ctx context.Context, ctx context.Context,
msg message.Msg, msg message.Msg,
action string, command []string, workDir *check.Absolute, action string, command []string, workDir *check.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) { app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
flags := hst.FMultiarch
if dropShell {
flags |= hst.FTty
}
mustRunAppDropShell(ctx, msg, &hst.Config{ mustRunAppDropShell(ctx, msg, &hst.Config{
ID: app.ID, ID: app.ID,
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
{Path: workDir, Execute: true}, {Path: workDir, Execute: true},
@@ -80,8 +93,7 @@ func withCacheDir(
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
Multiarch: true,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: workDir.Append(fhs.Etc), Special: true}}, {FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: workDir.Append(fhs.Etc), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}}, {FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
@@ -98,6 +110,8 @@ func withCacheDir(
Path: pathShell, Path: pathShell,
Args: []string{bash, "-lc", strings.Join(command, " && ")}, Args: []string{bash, "-lc", strings.Join(command, " && ")},
Flags: flags,
}, },
}, dropShell, beforeFail) }, dropShell, beforeFail)
} }

View File

@@ -576,13 +576,12 @@ const (
func init() { func init() {
helperCommands = append(helperCommands, func(c command.Command) { helperCommands = append(helperCommands, func(c command.Command) {
c.Command("block", command.UsageInternal, func(args []string) error { c.Command("block", command.UsageInternal, func(args []string) error {
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
}
{
sig := make(chan os.Signal, 1) sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt) signal.Notify(sig, os.Interrupt)
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }() go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
} }
select {} select {}
}) })

View File

@@ -1,6 +1,7 @@
package seccomp_test package seccomp_test
import ( import (
"crypto/sha512"
"encoding/hex" "encoding/hex"
"hakurei.app/container/bits" "hakurei.app/container/bits"
@@ -12,18 +13,18 @@ type (
seccomp.ExportFlag seccomp.ExportFlag
bits.FilterPreset bits.FilterPreset
} }
bpfLookup map[bpfPreset][]byte bpfLookup map[bpfPreset][sha512.Size]byte
) )
func toHash(s string) []byte { func toHash(s string) [sha512.Size]byte {
if len(s) != 128 { if len(s) != sha512.Size*2 {
panic("bad sha512 string length") panic("bad sha512 string length")
} }
if v, err := hex.DecodeString(s); err != nil { if v, err := hex.DecodeString(s); err != nil {
panic(err.Error()) panic(err.Error())
} else if len(v) != 64 { } else if len(v) != sha512.Size {
panic("unreachable") panic("unreachable")
} else { } else {
return v return ([sha512.Size]byte)(v)
} }
} }

View File

@@ -9,14 +9,16 @@
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0])) #define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
uint32_t multiarch, uint32_t arch, uint32_t multiarch,
struct hakurei_syscall_rule *rules, struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags) { size_t rules_sz, hakurei_export_flag flags) {
int i; int i;
int last_allowed_family; int last_allowed_family;
int disallowed; int disallowed;
struct hakurei_syscall_rule *rule; struct hakurei_syscall_rule *rule;
void *buf;
size_t len = 0;
int32_t res = 0; /* refer to resPrefix for message */ int32_t res = 0; /* refer to resPrefix for message */
@@ -108,14 +110,26 @@ int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1)); SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
if (fd < 0) { if (allocate_p == 0) {
*ret_p = seccomp_load(ctx); *ret_p = seccomp_load(ctx);
if (*ret_p != 0) { if (*ret_p != 0) {
res = 7; res = 7;
goto out; goto out;
} }
} else { } else {
*ret_p = seccomp_export_bpf(ctx, fd); *ret_p = seccomp_export_bpf_mem(ctx, NULL, &len);
if (*ret_p != 0) {
res = 6;
goto out;
}
buf = hakurei_scmp_allocate(allocate_p, len);
if (buf == NULL) {
res = 4;
goto out;
}
*ret_p = seccomp_export_bpf_mem(ctx, buf, &len);
if (*ret_p != 0) { if (*ret_p != 0) {
res = 6; res = 6;
goto out; goto out;

View File

@@ -18,7 +18,8 @@ struct hakurei_syscall_rule {
struct scmp_arg_cmp *arg; struct scmp_arg_cmp *arg;
}; };
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, extern void *hakurei_scmp_allocate(uintptr_t f, size_t len);
uint32_t multiarch, int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
uint32_t arch, uint32_t multiarch,
struct hakurei_syscall_rule *rules, struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags); size_t rules_sz, hakurei_export_flag flags);

View File

@@ -3,7 +3,7 @@ package seccomp
/* /*
#cgo linux pkg-config: --static libseccomp #cgo linux pkg-config: --static libseccomp
#include <libseccomp-helper.h> #include "libseccomp-helper.h"
#include <sys/personality.h> #include <sys/personality.h>
*/ */
import "C" import "C"
@@ -11,23 +11,21 @@ import (
"errors" "errors"
"fmt" "fmt"
"runtime" "runtime"
"runtime/cgo"
"syscall" "syscall"
"unsafe" "unsafe"
) )
const ( // ErrInvalidRules is returned for a zero-length rules slice.
PER_LINUX = C.PER_LINUX var ErrInvalidRules = errors.New("invalid native rules slice")
PER_LINUX32 = C.PER_LINUX32
)
var (
ErrInvalidRules = errors.New("invalid native rules slice")
)
// LibraryError represents a libseccomp error. // LibraryError represents a libseccomp error.
type LibraryError struct { type LibraryError struct {
// User facing description of the libseccomp function returning the error.
Prefix string Prefix string
// Negated errno value returned by libseccomp.
Seccomp syscall.Errno Seccomp syscall.Errno
// Global errno value on return.
Errno error Errno error
} }
@@ -56,7 +54,9 @@ func (e *LibraryError) Is(err error) bool {
} }
type ( type (
// ScmpSyscall represents a syscall number passed to libseccomp via [NativeRule.Syscall].
ScmpSyscall = C.int ScmpSyscall = C.int
// ScmpErrno represents an errno value passed to libseccomp via [NativeRule.Errno].
ScmpErrno = C.int ScmpErrno = C.int
) )
@@ -88,12 +88,23 @@ var resPrefix = [...]string{
3: "seccomp_arch_add failed (multiarch)", 3: "seccomp_arch_add failed (multiarch)",
4: "internal libseccomp failure", 4: "internal libseccomp failure",
5: "seccomp_rule_add failed", 5: "seccomp_rule_add failed",
6: "seccomp_export_bpf failed", 6: "seccomp_export_bpf_mem failed",
7: "seccomp_load failed", 7: "seccomp_load failed",
} }
// Export streams filter contents to fd, or installs it to the current process if fd < 0. // cbAllocateBuffer is the function signature for the function handle passed to hakurei_export_filter
func Export(fd int, rules []NativeRule, flags ExportFlag) error { // which allocates the buffer that the resulting bpf program is copied into, and writes its slice header
// to a value held by the caller.
type cbAllocateBuffer = func(len C.size_t) (buf unsafe.Pointer)
//export hakurei_scmp_allocate
func hakurei_scmp_allocate(f C.uintptr_t, len C.size_t) (buf unsafe.Pointer) {
return cgo.Handle(f).Value().(cbAllocateBuffer)(len)
}
// makeFilter generates a bpf program from a slice of [NativeRule] and writes the resulting byte slice to p.
// The filter is installed to the current process if p is nil.
func makeFilter(rules []NativeRule, flags ExportFlag, p *[]byte) error {
if len(rules) == 0 { if len(rules) == 0 {
return ErrInvalidRules return ErrInvalidRules
} }
@@ -117,33 +128,56 @@ func Export(fd int, rules []NativeRule, flags ExportFlag) error {
var ret C.int var ret C.int
var rulesPinner runtime.Pinner var scmpPinner runtime.Pinner
for i := range rules { for i := range rules {
rule := &rules[i] rule := &rules[i]
rulesPinner.Pin(rule) scmpPinner.Pin(rule)
if rule.Arg != nil { if rule.Arg != nil {
rulesPinner.Pin(rule.Arg) scmpPinner.Pin(rule.Arg)
} }
} }
res, err := C.hakurei_export_filter(
&ret, C.int(fd), var allocateP cgo.Handle
if p != nil {
allocateP = cgo.NewHandle(func(len C.size_t) (buf unsafe.Pointer) {
// this is so the slice header gets a Go pointer
*p = make([]byte, len)
buf = unsafe.Pointer(unsafe.SliceData(*p))
scmpPinner.Pin(buf)
return
})
}
res, err := C.hakurei_scmp_make_filter(
&ret, C.uintptr_t(allocateP),
arch, multiarch, arch, multiarch,
(*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])), (*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])),
C.size_t(len(rules)), C.size_t(len(rules)),
flags, flags,
) )
rulesPinner.Unpin() scmpPinner.Unpin()
if p != nil {
allocateP.Delete()
}
if prefix := resPrefix[res]; prefix != "" { if prefix := resPrefix[res]; prefix != "" {
return &LibraryError{ return &LibraryError{prefix, syscall.Errno(-ret), err}
prefix,
-syscall.Errno(ret),
err,
}
} }
return err return err
} }
// Export generates a bpf program from a slice of [NativeRule].
// Errors returned by libseccomp is wrapped in [LibraryError].
func Export(rules []NativeRule, flags ExportFlag) (data []byte, err error) {
err = makeFilter(rules, flags, &data)
return
}
// Load generates a bpf program from a slice of [NativeRule] and enforces it on the current process.
// Errors returned by libseccomp is wrapped in [LibraryError].
func Load(rules []NativeRule, flags ExportFlag) error { return makeFilter(rules, flags, nil) }
// ScmpCompare is the equivalent of scmp_compare; // ScmpCompare is the equivalent of scmp_compare;
// Comparison operators // Comparison operators
type ScmpCompare = C.enum_scmp_compare type ScmpCompare = C.enum_scmp_compare
@@ -184,7 +218,15 @@ type ScmpArgCmp struct {
DatumA, DatumB ScmpDatum DatumA, DatumB ScmpDatum
} }
// only used for testing const (
// PersonaLinux is passed in a [ScmpDatum] for filtering calls to syscall.SYS_PERSONALITY.
PersonaLinux = C.PER_LINUX
// PersonaLinux32 is passed in a [ScmpDatum] for filtering calls to syscall.SYS_PERSONALITY.
PersonaLinux32 = C.PER_LINUX32
)
// syscallResolveName resolves a syscall number by name via seccomp_syscall_resolve_name.
// This function is only for testing the lookup tables and included here for convenience.
func syscallResolveName(s string) (trap int) { func syscallResolveName(s string) (trap int) {
v := C.CString(s) v := C.CString(s)
trap = int(C.seccomp_syscall_resolve_name(v)) trap = int(C.seccomp_syscall_resolve_name(v))

View File

@@ -3,8 +3,6 @@ package seccomp_test
import ( import (
"crypto/sha512" "crypto/sha512"
"errors" "errors"
"io"
"slices"
"syscall" "syscall"
"testing" "testing"
@@ -12,6 +10,67 @@ import (
. "hakurei.app/container/seccomp" . "hakurei.app/container/seccomp"
) )
func TestLibraryError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
sample *LibraryError
want string
wantIs bool
compare error
}{
{
"full",
&LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
"seccomp_export_bpf failed: operation canceled (bad file descriptor)",
true,
&LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
},
{
"errno only",
&LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM},
"seccomp_init failed: cannot allocate memory",
false,
nil,
},
{
"seccomp only",
&LibraryError{Prefix: "internal libseccomp failure", Seccomp: syscall.EFAULT},
"internal libseccomp failure: bad address",
true,
syscall.EFAULT,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if errors.Is(tc.sample, tc.compare) != tc.wantIs {
t.Errorf("errors.Is(%#v, %#v) did not return %v",
tc.sample, tc.compare, tc.wantIs)
}
if got := tc.sample.Error(); got != tc.want {
t.Errorf("Error: %q, want %q",
got, tc.want)
}
})
}
t.Run("invalid", func(t *testing.T) {
t.Parallel()
wantPanic := "invalid libseccomp error"
defer func() {
if r := recover(); r != wantPanic {
t.Errorf("panic: %q, want %q", r, wantPanic)
}
}()
_ = new(LibraryError).Error()
})
}
func TestExport(t *testing.T) { func TestExport(t *testing.T) {
t.Parallel() t.Parallel()
@@ -38,61 +97,34 @@ func TestExport(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
e := New(Preset(tc.presets, tc.flags), tc.flags)
want := bpfExpected[bpfPreset{tc.flags, tc.presets}] want := bpfExpected[bpfPreset{tc.flags, tc.presets}]
digest := sha512.New() if data, err := Export(Preset(tc.presets, tc.flags), tc.flags); (err != nil) != tc.wantErr {
t.Errorf("Export: error = %v, wantErr %v", err, tc.wantErr)
if _, err := io.Copy(digest, e); (err != nil) != tc.wantErr {
t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr)
return return
} } else if got := sha512.Sum512(data); got != want {
if err := e.Close(); err != nil { t.Fatalf("Export: hash = %x, want %x", got, want)
t.Errorf("Close: error = %v", err)
}
if got := digest.Sum(nil); !slices.Equal(got, want) {
t.Fatalf("Export: hash = %x, want %x",
got, want)
return return
} }
}) })
} }
t.Run("close without use", func(t *testing.T) {
e := New(Preset(0, 0), 0)
if err := e.Close(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("Close: error = %v", err)
return
}
})
t.Run("close partial read", func(t *testing.T) {
e := New(Preset(0, 0), 0)
if _, err := e.Read(nil); err != nil {
t.Errorf("Read: error = %v", err)
return
}
// the underlying implementation uses buffered io, so the outcome of this is nondeterministic;
// that is not harmful however, so both outcomes are checked for here
if err := e.Close(); err != nil &&
(!errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF)) {
t.Errorf("Close: error = %v", err)
return
}
})
} }
func BenchmarkExport(b *testing.B) { func BenchmarkExport(b *testing.B) {
buf := make([]byte, 8) const exportFlags = AllowMultiarch | AllowCAN | AllowBluetooth
const presetFlags = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel | PresetLinux32
var want = bpfExpected[bpfPreset{exportFlags, presetFlags}]
for b.Loop() { for b.Loop() {
e := New( data, err := Export(Preset(presetFlags, exportFlags), exportFlags)
Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32,
AllowMultiarch|AllowCAN|AllowBluetooth), b.StopTimer()
AllowMultiarch|AllowCAN|AllowBluetooth) if err != nil {
if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil { b.Fatalf("Export: error = %v", err)
b.Fatalf("cannot export: %v", err)
} }
if err := e.Close(); err != nil { if got := sha512.Sum512(data); got != want {
b.Fatalf("cannot close exporter: %v", err) b.Fatalf("Export: hash = %x, want %x", got, want)
return
} }
b.StartTimer()
} }
} }

View File

@@ -9,9 +9,9 @@ import (
) )
func Preset(presets bits.FilterPreset, flags ExportFlag) (rules []NativeRule) { func Preset(presets bits.FilterPreset, flags ExportFlag) (rules []NativeRule) {
allowedPersonality := PER_LINUX allowedPersonality := PersonaLinux
if presets&bits.PresetLinux32 != 0 { if presets&bits.PresetLinux32 != 0 {
allowedPersonality = PER_LINUX32 allowedPersonality = PersonaLinux32
} }
presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality)) presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality))

View File

@@ -1,72 +0,0 @@
package seccomp
import (
"context"
"errors"
"syscall"
"hakurei.app/helper/proc"
)
// New returns an inactive Encoder instance.
func New(rules []NativeRule, flags ExportFlag) *Encoder { return &Encoder{newExporter(rules, flags)} }
// Load loads a filter into the kernel.
func Load(rules []NativeRule, flags ExportFlag) error { return Export(-1, rules, flags) }
/*
An Encoder writes a BPF program to an output stream.
Methods of Encoder are not safe for concurrent use.
An Encoder must not be copied after first use.
*/
type Encoder struct{ *exporter }
func (e *Encoder) Read(p []byte) (n int, err error) {
if err = e.prepare(); err != nil {
return
}
return e.r.Read(p)
}
func (e *Encoder) Close() error {
if e.r == nil {
return syscall.EINVAL
}
// this hangs if the cgo thread fails to exit
return errors.Join(e.closeWrite(), <-e.exportErr)
}
// NewFile returns an instance of exporter implementing [proc.File].
func NewFile(rules []NativeRule, flags ExportFlag) proc.File {
return &File{rules: rules, flags: flags}
}
// File implements [proc.File] and provides access to the read end of exporter pipe.
type File struct {
rules []NativeRule
flags ExportFlag
proc.BaseFile
}
func (f *File) ErrCount() int { return 2 }
func (f *File) Fulfill(ctx context.Context, dispatchErr func(error)) error {
e := newExporter(f.rules, f.flags)
if err := e.prepare(); err != nil {
return err
}
f.Set(e.r)
go func() {
select {
case err := <-e.exportErr:
dispatchErr(nil)
dispatchErr(err)
case <-ctx.Done():
dispatchErr(e.closeWrite())
dispatchErr(<-e.exportErr)
}
}()
return nil
}

View File

@@ -1,60 +0,0 @@
// Package seccomp provides high level wrappers around libseccomp.
package seccomp
import (
"os"
"runtime"
"sync"
)
type exporter struct {
rules []NativeRule
flags ExportFlag
r, w *os.File
prepareOnce sync.Once
prepareErr error
closeOnce sync.Once
closeErr error
exportErr <-chan error
}
func (e *exporter) prepare() error {
e.prepareOnce.Do(func() {
if r, w, err := os.Pipe(); err != nil {
e.prepareErr = err
return
} else {
e.r, e.w = r, w
}
ec := make(chan error, 1)
go func(fd uintptr) {
ec <- Export(int(fd), e.rules, e.flags)
close(ec)
_ = e.closeWrite()
runtime.KeepAlive(e.w)
}(e.w.Fd())
e.exportErr = ec
runtime.SetFinalizer(e, (*exporter).closeWrite)
})
return e.prepareErr
}
func (e *exporter) closeWrite() error {
e.closeOnce.Do(func() {
if e.w == nil {
panic("closeWrite called on invalid exporter")
}
e.closeErr = e.w.Close()
// no need for a finalizer anymore
runtime.SetFinalizer(e, nil)
})
return e.closeErr
}
func newExporter(rules []NativeRule, flags ExportFlag) *exporter {
return &exporter{rules: rules, flags: flags}
}

View File

@@ -1,71 +0,0 @@
package seccomp_test
import (
"errors"
"runtime"
"syscall"
"testing"
"hakurei.app/container/seccomp"
)
func TestLibraryError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
sample *seccomp.LibraryError
want string
wantIs bool
compare error
}{
{
"full",
&seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
"seccomp_export_bpf failed: operation canceled (bad file descriptor)",
true,
&seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
},
{
"errno only",
&seccomp.LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM},
"seccomp_init failed: cannot allocate memory",
false,
nil,
},
{
"seccomp only",
&seccomp.LibraryError{Prefix: "internal libseccomp failure", Seccomp: syscall.EFAULT},
"internal libseccomp failure: bad address",
true,
syscall.EFAULT,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if errors.Is(tc.sample, tc.compare) != tc.wantIs {
t.Errorf("errors.Is(%#v, %#v) did not return %v",
tc.sample, tc.compare, tc.wantIs)
}
if got := tc.sample.Error(); got != tc.want {
t.Errorf("Error: %q, want %q",
got, tc.want)
}
})
}
t.Run("invalid", func(t *testing.T) {
t.Parallel()
wantPanic := "invalid libseccomp error"
defer func() {
if r := recover(); r != wantPanic {
t.Errorf("panic: %q, want %q", r, wantPanic)
}
}()
runtime.KeepAlive(new(seccomp.LibraryError).Error())
})
}

View File

@@ -35,7 +35,7 @@ func (a argsWt) String() string {
} }
// NewCheckedArgs returns a checked null-terminated argument writer for a copy of args. // NewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
func NewCheckedArgs(args []string) (wt io.WriterTo, err error) { func NewCheckedArgs(args ...string) (wt io.WriterTo, err error) {
a := make(argsWt, len(args)) a := make(argsWt, len(args))
for i, arg := range args { for i, arg := range args {
a[i], err = syscall.ByteSliceFromString(arg) a[i], err = syscall.ByteSliceFromString(arg)
@@ -49,8 +49,8 @@ func NewCheckedArgs(args []string) (wt io.WriterTo, err error) {
// MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args. // MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
// If s contains a NUL byte this function panics instead of returning an error. // If s contains a NUL byte this function panics instead of returning an error.
func MustNewCheckedArgs(args []string) io.WriterTo { func MustNewCheckedArgs(args ...string) io.WriterTo {
a, err := NewCheckedArgs(args) a, err := NewCheckedArgs(args...)
if err != nil { if err != nil {
panic(err.Error()) panic(err.Error())
} }

View File

@@ -22,7 +22,7 @@ func TestNewCheckedArgs(t *testing.T) {
t.Parallel() t.Parallel()
args := []string{"\x00"} args := []string{"\x00"}
if _, err := helper.NewCheckedArgs(args); !errors.Is(err, syscall.EINVAL) { if _, err := helper.NewCheckedArgs(args...); !errors.Is(err, syscall.EINVAL) {
t.Errorf("NewCheckedArgs: error = %v, wantErr %v", err, syscall.EINVAL) t.Errorf("NewCheckedArgs: error = %v, wantErr %v", err, syscall.EINVAL)
} }
@@ -36,6 +36,6 @@ func TestNewCheckedArgs(t *testing.T) {
t.Errorf("MustNewCheckedArgs: panic = %v, wantPanic %v", r, wantPanic) t.Errorf("MustNewCheckedArgs: panic = %v, wantPanic %v", r, wantPanic)
} }
}() }()
helper.MustNewCheckedArgs(badPayload) helper.MustNewCheckedArgs(badPayload...)
}) })
} }

View File

@@ -27,7 +27,7 @@ var (
} }
wantPayload = strings.Join(wantArgs, "\x00") + "\x00" wantPayload = strings.Join(wantArgs, "\x00") + "\x00"
argsWt = helper.MustNewCheckedArgs(wantArgs) argsWt = helper.MustNewCheckedArgs(wantArgs...)
) )
func argF(argsFd, statFd int) []string { func argF(argsFd, statFd int) []string {

View File

@@ -3,37 +3,12 @@ package hst
import ( import (
"errors" "errors"
"strconv" "strconv"
"time"
"hakurei.app/container/check" "hakurei.app/container/check"
) )
// PrivateTmp is a private writable path in a hakurei container. // Config configures an application container, implemented in internal/app.
const PrivateTmp = "/.hakurei" type Config struct {
// AbsPrivateTmp is a [check.Absolute] representation of [PrivateTmp].
var AbsPrivateTmp = check.MustAbs(PrivateTmp)
const (
// WaitDelayDefault is used when WaitDelay has its zero value.
WaitDelayDefault = 5 * time.Second
// WaitDelayMax is used if WaitDelay exceeds its value.
WaitDelayMax = 30 * time.Second
// IdentityMin is the minimum value of [Config.Identity]. This is enforced by cmd/hsu.
IdentityMin = 0
// IdentityMax is the maximum value of [Config.Identity]. This is enforced by cmd/hsu.
IdentityMax = 9999
// ShimExitRequest is returned when the priv side process requests shim exit.
ShimExitRequest = 254
// ShimExitOrphan is returned when the shim is orphaned before priv side delivers a signal.
ShimExitOrphan = 3
)
type (
// Config configures an application container, implemented in internal/app.
Config struct {
// Reverse-DNS style configured arbitrary identifier string. // Reverse-DNS style configured arbitrary identifier string.
// Passed to wayland security-context-v1 and used as part of defaults in dbus session proxy. // Passed to wayland security-context-v1 and used as part of defaults in dbus session proxy.
ID string `json:"id"` ID string `json:"id"`
@@ -51,8 +26,8 @@ type (
// and the bare socket is made available to the container. // and the bare socket is made available to the container.
DirectWayland bool `json:"direct_wayland,omitempty"` DirectWayland bool `json:"direct_wayland,omitempty"`
// Extra acl update ops to perform before setuid. // Extra acl updates to perform before setuid.
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"` ExtraPerms []ExtraPermConfig `json:"extra_perms,omitempty"`
// Numerical application id, passed to hsu, used to derive init user namespace credentials. // Numerical application id, passed to hsu, used to derive init user namespace credentials.
Identity int `json:"identity"` Identity int `json:"identity"`
@@ -61,64 +36,7 @@ type (
// High level configuration applied to the underlying [container]. // High level configuration applied to the underlying [container].
Container *ContainerConfig `json:"container"` Container *ContainerConfig `json:"container"`
} }
// ContainerConfig describes the container configuration to be applied to an underlying [container].
ContainerConfig struct {
// Container UTS namespace hostname.
Hostname string `json:"hostname,omitempty"`
// Duration in nanoseconds to wait for after interrupting the initial process.
// Defaults to [WaitDelayDefault] if zero, or [WaitDelayMax] if greater than [WaitDelayMax].
// Values lesser than zero is equivalent to zero, bypassing [WaitDelayDefault].
WaitDelay time.Duration `json:"wait_delay,omitempty"`
// Emit Flatpak-compatible seccomp filter programs.
SeccompCompat bool `json:"seccomp_compat,omitempty"`
// Allow ptrace and friends.
Devel bool `json:"devel,omitempty"`
// Allow userns creation and container setup syscalls.
Userns bool `json:"userns,omitempty"`
// Share host net namespace.
HostNet bool `json:"host_net,omitempty"`
// Share abstract unix socket scope.
HostAbstract bool `json:"host_abstract,omitempty"`
// Allow dangerous terminal I/O (faking input).
Tty bool `json:"tty,omitempty"`
// Allow multiarch.
Multiarch bool `json:"multiarch,omitempty"`
// Initial process environment variables.
Env map[string]string `json:"env"`
/* Map target user uid to privileged user uid in the container user namespace.
Some programs fail to connect to dbus session running as a different uid,
this option works around it by mapping priv-side caller uid in container. */
MapRealUID bool `json:"map_real_uid"`
// Mount /dev/ from the init mount namespace as-is in the container mount namespace.
Device bool `json:"device,omitempty"`
/* Container mount points.
If the first element targets /, it is inserted early and excluded from path hiding. */
Filesystem []FilesystemConfigJSON `json:"filesystem"`
// String used as the username of the emulated user, validated against the default NAME_REGEX from adduser.
// Defaults to passwd name of target uid or chronos.
Username string `json:"username,omitempty"`
// Pathname of shell in the container filesystem to use for the emulated user.
Shell *check.Absolute `json:"shell"`
// Directory in the container filesystem to enter and use as the home directory of the emulated user.
Home *check.Absolute `json:"home"`
// Pathname to executable file in the container filesystem.
Path *check.Absolute `json:"path,omitempty"`
// Final args passed to the initial program.
Args []string `json:"args"`
}
)
var ( var (
// ErrConfigNull is returned by [Config.Validate] for an invalid configuration that contains a null value for any // ErrConfigNull is returned by [Config.Validate] for an invalid configuration that contains a null value for any
@@ -168,15 +86,21 @@ func (config *Config) Validate() error {
return nil return nil
} }
// ExtraPermConfig describes an acl update op. // ExtraPermConfig describes an acl update to perform before setuid.
type ExtraPermConfig struct { type ExtraPermConfig struct {
// Whether to create Path as a directory if it does not exist.
Ensure bool `json:"ensure,omitempty"` Ensure bool `json:"ensure,omitempty"`
// Pathname to act on.
Path *check.Absolute `json:"path"` Path *check.Absolute `json:"path"`
// Whether to set ACL_READ for the target user.
Read bool `json:"r,omitempty"` Read bool `json:"r,omitempty"`
// Whether to set ACL_WRITE for the target user.
Write bool `json:"w,omitempty"` Write bool `json:"w,omitempty"`
// Whether to set ACL_EXECUTE for the target user.
Execute bool `json:"x,omitempty"` Execute bool `json:"x,omitempty"`
} }
// String returns a checked string representation of [ExtraPermConfig].
func (e *ExtraPermConfig) String() string { func (e *ExtraPermConfig) String() string {
if e == nil || e.Path == nil { if e == nil || e.Path == nil {
return "<invalid>" return "<invalid>"

189
hst/container.go Normal file
View File

@@ -0,0 +1,189 @@
package hst
import (
"encoding/json"
"syscall"
"time"
"hakurei.app/container/check"
)
// PrivateTmp is a private writable path in a hakurei container.
const PrivateTmp = "/.hakurei"
// AbsPrivateTmp is a [check.Absolute] representation of [PrivateTmp].
var AbsPrivateTmp = check.MustAbs(PrivateTmp)
const (
// WaitDelayDefault is used when WaitDelay has its zero value.
WaitDelayDefault = 5 * time.Second
// WaitDelayMax is used if WaitDelay exceeds its value.
WaitDelayMax = 30 * time.Second
// IdentityMin is the minimum value of [Config.Identity]. This is enforced by cmd/hsu.
IdentityMin = 0
// IdentityMax is the maximum value of [Config.Identity]. This is enforced by cmd/hsu.
IdentityMax = 9999
// ShimExitRequest is returned when the priv side process requests shim exit.
ShimExitRequest = 254
// ShimExitOrphan is returned when the shim is orphaned before priv side delivers a signal.
ShimExitOrphan = 3
)
const (
// FMultiarch unblocks syscalls required for multiarch to work on applicable targets.
FMultiarch uintptr = 1 << iota
// FSeccompCompat causes emitted seccomp filter programs to be identical to Flatpak.
FSeccompCompat
// FDevel unblocks ptrace and friends.
FDevel
// FUserns unblocks userns creation and container setup syscalls.
FUserns
// FHostNet skips net namespace creation.
FHostNet
// FHostAbstract skips setting up abstract unix socket scope.
FHostAbstract
// FTty unblocks dangerous terminal I/O (faking input).
FTty
// FMapRealUID maps the target user uid to the privileged user uid in the container user namespace.
// Some programs fail to connect to dbus session running as a different uid,
// this option works around it by mapping priv-side caller uid in container.
FMapRealUID
// FDevice mount /dev/ from the init mount namespace as-is in the container mount namespace.
FDevice
fMax
// FAll is [ContainerConfig.Flags] with all currently defined bits set.
FAll = fMax - 1
)
// ContainerConfig describes the container configuration to be applied to an underlying [container].
type ContainerConfig struct {
// Container UTS namespace hostname.
Hostname string `json:"hostname,omitempty"`
// Duration in nanoseconds to wait for after interrupting the initial process.
// Defaults to [WaitDelayDefault] if zero, or [WaitDelayMax] if greater than [WaitDelayMax].
// Values lesser than zero is equivalent to zero, bypassing [WaitDelayDefault].
WaitDelay time.Duration `json:"wait_delay,omitempty"`
// Initial process environment variables.
Env map[string]string `json:"env"`
/* Container mount points.
If the first element targets /, it is inserted early and excluded from path hiding. */
Filesystem []FilesystemConfigJSON `json:"filesystem"`
// String used as the username of the emulated user, validated against the default NAME_REGEX from adduser.
// Defaults to passwd name of target uid or chronos.
Username string `json:"username,omitempty"`
// Pathname of shell in the container filesystem to use for the emulated user.
Shell *check.Absolute `json:"shell"`
// Directory in the container filesystem to enter and use as the home directory of the emulated user.
Home *check.Absolute `json:"home"`
// Pathname to executable file in the container filesystem.
Path *check.Absolute `json:"path,omitempty"`
// Final args passed to the initial program.
Args []string `json:"args"`
// Flags holds boolean options of [ContainerConfig].
Flags uintptr `json:"-"`
}
// ContainerConfigF is [ContainerConfig] stripped of its methods.
// The [ContainerConfig.Flags] field does not survive a [json] round trip.
type ContainerConfigF ContainerConfig
// containerConfigJSON is the [json] representation of [ContainerConfig].
type containerConfigJSON = struct {
*ContainerConfigF
// Corresponds to [FSeccompCompat].
SeccompCompat bool `json:"seccomp_compat,omitempty"`
// Corresponds to [FDevel].
Devel bool `json:"devel,omitempty"`
// Corresponds to [FUserns].
Userns bool `json:"userns,omitempty"`
// Corresponds to [FHostNet].
HostNet bool `json:"host_net,omitempty"`
// Corresponds to [FHostAbstract].
HostAbstract bool `json:"host_abstract,omitempty"`
// Corresponds to [FTty].
Tty bool `json:"tty,omitempty"`
// Corresponds to [FMultiarch].
Multiarch bool `json:"multiarch,omitempty"`
// Corresponds to [FMapRealUID].
MapRealUID bool `json:"map_real_uid"`
// Corresponds to [FDevice].
Device bool `json:"device,omitempty"`
}
func (c *ContainerConfig) MarshalJSON() ([]byte, error) {
if c == nil {
return nil, syscall.EINVAL
}
return json.Marshal(&containerConfigJSON{
ContainerConfigF: (*ContainerConfigF)(c),
SeccompCompat: c.Flags&FSeccompCompat != 0,
Devel: c.Flags&FDevel != 0,
Userns: c.Flags&FUserns != 0,
HostNet: c.Flags&FHostNet != 0,
HostAbstract: c.Flags&FHostAbstract != 0,
Tty: c.Flags&FTty != 0,
Multiarch: c.Flags&FMultiarch != 0,
MapRealUID: c.Flags&FMapRealUID != 0,
Device: c.Flags&FDevice != 0,
})
}
func (c *ContainerConfig) UnmarshalJSON(data []byte) error {
if c == nil {
return syscall.EINVAL
}
v := new(containerConfigJSON)
if err := json.Unmarshal(data, &v); err != nil {
return err
}
*c = *(*ContainerConfig)(v.ContainerConfigF)
if v.SeccompCompat {
c.Flags |= FSeccompCompat
}
if v.Devel {
c.Flags |= FDevel
}
if v.Userns {
c.Flags |= FUserns
}
if v.HostNet {
c.Flags |= FHostNet
}
if v.HostAbstract {
c.Flags |= FHostAbstract
}
if v.Tty {
c.Flags |= FTty
}
if v.Multiarch {
c.Flags |= FMultiarch
}
if v.MapRealUID {
c.Flags |= FMapRealUID
}
if v.Device {
c.Flags |= FDevice
}
return nil
}

76
hst/container_test.go Normal file
View File

@@ -0,0 +1,76 @@
package hst_test
import (
"encoding/json"
"errors"
"reflect"
"syscall"
"testing"
"hakurei.app/hst"
)
func TestContainerConfig(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
c *hst.ContainerConfig
data string
}{
{"nil", nil, "null"},
{"zero", new(hst.ContainerConfig),
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"map_real_uid":false}`},
{"seccomp compat", &hst.ContainerConfig{Flags: hst.FSeccompCompat},
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"seccomp_compat":true,"map_real_uid":false}`},
{"hostnet hostabstract", &hst.ContainerConfig{Flags: hst.FHostNet | hst.FHostAbstract},
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"host_net":true,"host_abstract":true,"map_real_uid":false}`},
{"hostnet hostabstract mapuid", &hst.ContainerConfig{Flags: hst.FHostNet | hst.FHostAbstract | hst.FMapRealUID},
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"host_net":true,"host_abstract":true,"map_real_uid":true}`},
{"all", &hst.ContainerConfig{Flags: hst.FAll},
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"seccomp_compat":true,"devel":true,"userns":true,"host_net":true,"host_abstract":true,"tty":true,"multiarch":true,"map_real_uid":true,"device":true}`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("marshal", func(t *testing.T) {
t.Parallel()
if got, err := json.Marshal(tc.c); err != nil {
t.Fatalf("Marshal: error = %v", err)
} else if string(got) != tc.data {
t.Errorf("Marshal:\n%s, want\n%s", string(got), tc.data)
}
})
t.Run("unmarshal", func(t *testing.T) {
t.Parallel()
{
got := new(hst.ContainerConfig)
if err := json.Unmarshal([]byte(tc.data), &got); err != nil {
t.Fatalf("Unmarshal: error = %v", err)
}
if !reflect.DeepEqual(got, tc.c) {
t.Errorf("Unmarshal: %v, want %v", got, tc.c)
}
}
})
})
}
t.Run("passthrough", func(t *testing.T) {
t.Parallel()
if _, err := (*hst.ContainerConfig)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("MarshalJSON: error = %v", err)
}
if err := (*hst.ContainerConfig)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) {
t.Errorf("UnmarshalJSON: error = %v", err)
}
if err := new(hst.ContainerConfig).UnmarshalJSON([]byte{}); err == nil {
t.Errorf("UnmarshalJSON: error = %v", err)
}
})
}

View File

@@ -3,6 +3,7 @@ package hst
import ( import (
"errors" "errors"
"math"
"net" "net"
"os" "os"
@@ -87,7 +88,7 @@ func Template() *Config {
}, },
DirectWayland: false, DirectWayland: false,
ExtraPerms: []*ExtraPermConfig{ ExtraPerms: []ExtraPermConfig{
{Path: fhs.AbsVarLib.Append("hakurei/u0"), Ensure: true, Execute: true}, {Path: fhs.AbsVarLib.Append("hakurei/u0"), Ensure: true, Execute: true},
{Path: fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"), Read: true, Write: true, Execute: true}, {Path: fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"), Read: true, Write: true, Execute: true},
}, },
@@ -97,16 +98,7 @@ func Template() *Config {
Container: &ContainerConfig{ Container: &ContainerConfig{
Hostname: "localhost", Hostname: "localhost",
Devel: true,
Userns: true,
HostNet: true,
HostAbstract: true,
Device: true,
WaitDelay: -1, WaitDelay: -1,
SeccompCompat: true,
Tty: true,
Multiarch: true,
MapRealUID: true,
// example API credentials pulled from Google Chrome // example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER // DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{ Env: map[string]string{
@@ -143,6 +135,9 @@ func Template() *Config {
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland", "--ozone-platform=wayland",
}, },
// Set all bits here so new flags trip the template test.
Flags: math.MaxUint,
}, },
} }
} }

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
"net" "net"
"os" "os"
"reflect"
"syscall" "syscall"
"testing" "testing"
@@ -164,20 +165,11 @@ func TestTemplate(t *testing.T) {
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1, "wait_delay": -1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"device": true,
"filesystem": [ "filesystem": [
{ {
"type": "bind", "type": "bind",
@@ -243,14 +235,41 @@ func TestTemplate(t *testing.T) {
"--disable-smooth-scrolling", "--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
] ],
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"map_real_uid": true,
"device": true
} }
}` }`
t.Run("marshal", func(t *testing.T) {
t.Parallel()
if p, err := json.MarshalIndent(hst.Template(), "", "\t"); err != nil { if p, err := json.MarshalIndent(hst.Template(), "", "\t"); err != nil {
t.Fatalf("cannot marshal: %v", err) t.Fatalf("cannot marshal: %v", err)
} else if s := string(p); s != want { } else if s := string(p); s != want {
t.Fatalf("Template:\n%s\nwant:\n%s", t.Fatalf("Template:\n%s\nwant:\n%s",
s, want) s, want)
} }
})
t.Run("unmarshal", func(t *testing.T) {
t.Parallel()
var got *hst.Config
if err := json.Unmarshal([]byte(want), &got); err != nil {
t.Fatalf("Unmarshal: error = %v", err)
}
wantVal := hst.Template()
wantVal.Container.Flags = hst.FAll
if !reflect.DeepEqual(got, wantVal) {
t.Fatalf("Unmarshal: %#v, want %#v", got, wantVal)
}
})
} }

View File

@@ -25,6 +25,7 @@ import (
"hakurei.app/message" "hakurei.app/message"
"hakurei.app/system" "hakurei.app/system"
"hakurei.app/system/acl" "hakurei.app/system/acl"
"hakurei.app/system/dbus"
) )
func TestApp(t *testing.T) { func TestApp(t *testing.T) {
@@ -43,8 +44,6 @@ func TestApp(t *testing.T) {
{ {
"nixos permissive defaults no enablements", new(stubNixOS), "nixos permissive defaults no enablements", new(stubNixOS),
&hst.Config{Container: &hst.ContainerConfig{ &hst.Config{Container: &hst.ContainerConfig{
Userns: true, HostNet: true, HostAbstract: true, Tty: true,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{ {FilesystemConfig: &hst.FSBind{
Target: fhs.AbsRoot, Target: fhs.AbsRoot,
@@ -70,6 +69,8 @@ func TestApp(t *testing.T) {
Path: m("/run/current-system/sw/bin/zsh"), Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"/run/current-system/sw/bin/zsh"}, Args: []string{"/run/current-system/sw/bin/zsh"},
Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty,
}}, }},
state.ID{ state.ID{
0x4a, 0x45, 0x0b, 0x65, 0x4a, 0x45, 0x0b, 0x65,
@@ -161,8 +162,6 @@ func TestApp(t *testing.T) {
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse), Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Userns: true, HostNet: true, HostAbstract: true, Tty: true,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{ {FilesystemConfig: &hst.FSBind{
Target: fhs.AbsRoot, Target: fhs.AbsRoot,
@@ -193,6 +192,8 @@ func TestApp(t *testing.T) {
Path: m("/run/current-system/sw/bin/zsh"), Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"zsh", "-c", "exec chromium "}, Args: []string{"zsh", "-c", "exec chromium "},
Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty,
}, },
}, },
state.ID{ state.ID{
@@ -213,7 +214,7 @@ func TestApp(t *testing.T) {
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), acl.Execute). Ephemeral(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), acl.Execute).
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse")). Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse")).
MustProxyDBus(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), &hst.BusConfig{ MustProxyDBus(&hst.BusConfig{
Talk: []string{ Talk: []string{
"org.freedesktop.Notifications", "org.freedesktop.Notifications",
"org.freedesktop.FileManager1", "org.freedesktop.FileManager1",
@@ -235,13 +236,19 @@ func TestApp(t *testing.T) {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*", "org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
}, },
Filter: true, Filter: true,
}, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), &hst.BusConfig{ }, &hst.BusConfig{
Talk: []string{ Talk: []string{
"org.bluez", "org.bluez",
"org.freedesktop.Avahi", "org.freedesktop.Avahi",
"org.freedesktop.UPower", "org.freedesktop.UPower",
}, },
Filter: true, Filter: true,
}, dbus.ProxyPair{
"unix:path=/run/user/1971/bus",
"/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus",
}, dbus.ProxyPair{
"unix:path=/var/run/dbus/system_bus_socket",
"/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket",
}). }).
UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), acl.Read, acl.Write). UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), acl.Read, acl.Write).
UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), acl.Read, acl.Write), UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), acl.Read, acl.Write),
@@ -301,7 +308,7 @@ func TestApp(t *testing.T) {
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse), Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Userns: true, HostNet: true, MapRealUID: true, Env: nil, Env: nil,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
f(&hst.FSBind{Source: m("/bin")}), f(&hst.FSBind{Source: m("/bin")}),
f(&hst.FSBind{Source: m("/usr/bin/")}), f(&hst.FSBind{Source: m("/usr/bin/")}),
@@ -323,6 +330,8 @@ func TestApp(t *testing.T) {
Home: m("/var/lib/persist/module/hakurei/0/1"), Home: m("/var/lib/persist/module/hakurei/0/1"),
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"), Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
Flags: hst.FUserns | hst.FHostNet | hst.FMapRealUID,
}, },
SystemBus: &hst.BusConfig{ SystemBus: &hst.BusConfig{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"}, Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
@@ -364,7 +373,7 @@ func TestApp(t *testing.T) {
Ephemeral(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), acl.Execute). Ephemeral(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), acl.Execute).
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse")). Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse")).
Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711). Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711).
MustProxyDBus(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), &hst.BusConfig{ MustProxyDBus(&hst.BusConfig{
Talk: []string{ Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets", "org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
@@ -377,13 +386,19 @@ func TestApp(t *testing.T) {
}, },
Call: map[string]string{}, Broadcast: map[string]string{}, Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true, Filter: true,
}, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), &hst.BusConfig{ }, &hst.BusConfig{
Talk: []string{ Talk: []string{
"org.bluez", "org.bluez",
"org.freedesktop.Avahi", "org.freedesktop.Avahi",
"org.freedesktop.UPower", "org.freedesktop.UPower",
}, },
Filter: true, Filter: true,
}, dbus.ProxyPair{
"unix:path=/run/user/1971/bus",
"/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus",
}, dbus.ProxyPair{
"unix:path=/var/run/dbus/system_bus_socket",
"/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket",
}). }).
UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), acl.Read, acl.Write). UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), acl.Read, acl.Write).
UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), acl.Read, acl.Write), UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), acl.Read, acl.Write),
@@ -739,6 +754,10 @@ func (k *stubNixOS) overflowGid(message.Msg) int { return 65534 }
func (k *stubNixOS) mustHsuPath() *check.Absolute { return m("/proc/nonexistent/hsu") } func (k *stubNixOS) mustHsuPath() *check.Absolute { return m("/proc/nonexistent/hsu") }
func (k *stubNixOS) dbusAddress() (string, string) {
return "unix:path=/run/user/1971/bus", "unix:path=/var/run/dbus/system_bus_socket"
}
func (k *stubNixOS) fatalf(format string, v ...any) { panic(fmt.Sprintf(format, v...)) } func (k *stubNixOS) fatalf(format string, v ...any) { panic(fmt.Sprintf(format, v...)) }
func (k *stubNixOS) isVerbose() bool { return true } func (k *stubNixOS) isVerbose() bool { return true }

View File

@@ -13,6 +13,7 @@ import (
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/message" "hakurei.app/message"
"hakurei.app/system/dbus"
) )
// osFile represents [os.File]. // osFile represents [os.File].
@@ -63,6 +64,9 @@ type syscallDispatcher interface {
// mustHsuPath provides [internal.MustHsuPath]. // mustHsuPath provides [internal.MustHsuPath].
mustHsuPath() *check.Absolute mustHsuPath() *check.Absolute
// dbusAddress provides [dbus.Address].
dbusAddress() (session, system string)
// fatalf provides [log.Fatalf]. // fatalf provides [log.Fatalf].
fatalf(format string, v ...any) fatalf(format string, v ...any)
} }
@@ -99,4 +103,6 @@ func (direct) overflowGid(msg message.Msg) int { return container.OverflowGid(ms
func (direct) mustHsuPath() *check.Absolute { return internal.MustHsuPath() } func (direct) mustHsuPath() *check.Absolute { return internal.MustHsuPath() }
func (k direct) dbusAddress() (session, system string) { return dbus.Address() }
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) } func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }

View File

@@ -97,7 +97,7 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
if err := s.populateLocal(k, k); err != nil { if err := s.populateLocal(k, k); err != nil {
t.Fatalf("populateLocal: error = %v", err) t.Fatalf("populateLocal: error = %v", err)
} }
stateSys := s.newSys(config, newI()) stateSys := s.newSys(config, system.New(panicMsgContext{}, k, checkExpectUid))
if tc.pStateSys != nil { if tc.pStateSys != nil {
tc.pStateSys(stateSys) tc.pStateSys(stateSys)
} }
@@ -218,6 +218,12 @@ func (k *kstub) mustHsuPath() *check.Absolute {
return k.Expects("mustHsuPath").Ret.(*check.Absolute) return k.Expects("mustHsuPath").Ret.(*check.Absolute)
} }
func (k *kstub) dbusAddress() (session, system string) {
k.Helper()
ret := k.Expects("dbusAddress").Ret.([2]string)
return ret[0], ret[1]
}
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") } func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) } func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
@@ -320,4 +326,5 @@ func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachab
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") } func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") } func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") } func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") }
func (panicDispatcher) dbusAddress() (string, string) { panic("unreachable") }
func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") } func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") }

View File

@@ -94,7 +94,7 @@ func newOutcomeState(k syscallDispatcher, msg message.Msg, id *state.ID, config
s.Shim.WaitDelay = s.Container.WaitDelay s.Shim.WaitDelay = s.Container.WaitDelay
} }
if s.Container.MapRealUID { if s.Container.Flags&hst.FMapRealUID != 0 {
s.Mapuid, s.Mapgid = k.getuid(), k.getgid() s.Mapuid, s.Mapgid = k.getuid(), k.getgid()
} else { } else {
s.Mapuid, s.Mapgid = k.overflowUid(msg), k.overflowGid(msg) s.Mapuid, s.Mapgid = k.overflowUid(msg), k.overflowGid(msg)
@@ -156,7 +156,7 @@ type outcomeStateSys struct {
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only. // Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
directWayland bool directWayland bool
// Copied header from [hst.Config]. Safe for read by spFinalOp.toSystem only. // Copied header from [hst.Config]. Safe for read by spFinalOp.toSystem only.
extraPerms []*hst.ExtraPermConfig extraPerms []hst.ExtraPermConfig
// Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only. // Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only.
sessionBus, systemBus *hst.BusConfig sessionBus, systemBus *hst.BusConfig

View File

@@ -48,9 +48,9 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
const preallocateOpsCount = 1 << 5 const preallocateOpsCount = 1 << 5
state.params.Hostname = state.Container.Hostname state.params.Hostname = state.Container.Hostname
state.params.RetainSession = state.Container.Tty state.params.RetainSession = state.Container.Flags&hst.FTty != 0
state.params.HostNet = state.Container.HostNet state.params.HostNet = state.Container.Flags&hst.FHostNet != 0
state.params.HostAbstract = state.Container.HostAbstract state.params.HostAbstract = state.Container.Flags&hst.FHostAbstract != 0
if state.Container.Path == nil { if state.Container.Path == nil {
return newWithMessage("invalid program path") return newWithMessage("invalid program path")
@@ -67,24 +67,24 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
// this behaviour is implemented in the shim // this behaviour is implemented in the shim
state.params.ForwardCancel = state.Shim.WaitDelay > 0 state.params.ForwardCancel = state.Shim.WaitDelay > 0
if state.Container.Multiarch { if state.Container.Flags&hst.FMultiarch != 0 {
state.params.SeccompFlags |= seccomp.AllowMultiarch state.params.SeccompFlags |= seccomp.AllowMultiarch
} }
if !state.Container.SeccompCompat { if state.Container.Flags&hst.FSeccompCompat == 0 {
state.params.SeccompPresets |= bits.PresetExt state.params.SeccompPresets |= bits.PresetExt
} }
if !state.Container.Devel { if state.Container.Flags&hst.FDevel == 0 {
state.params.SeccompPresets |= bits.PresetDenyDevel state.params.SeccompPresets |= bits.PresetDenyDevel
} }
if !state.Container.Userns { if state.Container.Flags&hst.FUserns == 0 {
state.params.SeccompPresets |= bits.PresetDenyNS state.params.SeccompPresets |= bits.PresetDenyNS
} }
if !state.Container.Tty { if state.Container.Flags&hst.FTty == 0 {
state.params.SeccompPresets |= bits.PresetDenyTTY state.params.SeccompPresets |= bits.PresetDenyTTY
} }
if state.Container.MapRealUID { if state.Container.Flags&hst.FMapRealUID != 0 {
state.params.Uid = state.Mapuid state.params.Uid = state.Mapuid
state.params.Gid = state.Mapgid state.params.Gid = state.Mapgid
} }
@@ -106,7 +106,7 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
state.params. state.params.
Proc(fhs.AbsProc). Proc(fhs.AbsProc).
Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755) Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755)
if !state.Container.Device { if state.Container.Flags&hst.FDevice == 0 {
state.params.DevWritable(fhs.AbsDev, true) state.params.DevWritable(fhs.AbsDev, true)
} else { } else {
state.params.Bind(fhs.AbsDev, fhs.AbsDev, bits.BindWritable|bits.BindDevice) state.params.Bind(fhs.AbsDev, fhs.AbsDev, bits.BindWritable|bits.BindDevice)
@@ -275,7 +275,7 @@ func (s *spFilesystemOp) toContainer(state *outcomeStateParams) error {
} }
// no more configured paths beyond this point // no more configured paths beyond this point
if !state.Container.Device { if state.Container.Flags&hst.FDevice == 0 {
state.params.Remount(fhs.AbsDev, syscall.MS_RDONLY) state.params.Remount(fhs.AbsDev, syscall.MS_RDONLY)
} }
return nil return nil

View File

@@ -51,12 +51,7 @@ func TestSpParamsOp(t *testing.T) {
}, func() *hst.Config { }, func() *hst.Config {
c := hst.Template() c := hst.Template()
c.Container.Args = nil c.Container.Args = nil
c.Container.Multiarch = false c.Container.Flags = hst.FHostNet | hst.FHostAbstract | hst.FMapRealUID
c.Container.SeccompCompat = false
c.Container.Devel = false
c.Container.Userns = false
c.Container.Tty = false
c.Container.Device = false
return c return c
}, nil, []stub.Call{ }, nil, []stub.Call{
call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil), call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
@@ -65,8 +60,8 @@ func TestSpParamsOp(t *testing.T) {
// this op configures the container state and does not make calls during toContainer // this op configures the container state and does not make calls during toContainer
}, &container.Params{ }, &container.Params{
Hostname: config.Container.Hostname, Hostname: config.Container.Hostname,
HostNet: config.Container.HostNet, HostNet: true,
HostAbstract: config.Container.HostAbstract, HostAbstract: true,
Path: config.Container.Path, Path: config.Container.Path,
Args: []string{config.Container.Path.String()}, Args: []string{config.Container.Path.String()},
SeccompPresets: bits.PresetExt | bits.PresetDenyDevel | bits.PresetDenyNS | bits.PresetDenyTTY, SeccompPresets: bits.PresetExt | bits.PresetDenyDevel | bits.PresetDenyNS | bits.PresetDenyTTY,
@@ -109,9 +104,9 @@ func TestSpParamsOp(t *testing.T) {
// this op configures the container state and does not make calls during toContainer // this op configures the container state and does not make calls during toContainer
}, &container.Params{ }, &container.Params{
Hostname: config.Container.Hostname, Hostname: config.Container.Hostname,
RetainSession: config.Container.Tty, RetainSession: true,
HostNet: config.Container.HostNet, HostNet: true,
HostAbstract: config.Container.HostAbstract, HostAbstract: true,
Path: config.Container.Path, Path: config.Container.Path,
Args: config.Container.Args, Args: config.Container.Args,
SeccompFlags: seccomp.AllowMultiarch, SeccompFlags: seccomp.AllowMultiarch,
@@ -159,7 +154,7 @@ func TestSpFilesystemOp(t *testing.T) {
}}}, }}},
{FilesystemConfig: &hst.FSEphemeral{Target: hst.AbsPrivateTmp}}, {FilesystemConfig: &hst.FSEphemeral{Target: hst.AbsPrivateTmp}},
} }
c.Container.Device = false c.Container.Flags &= ^hst.FDevice
return c return c
} }
configSmall := newConfigSmall() configSmall := newConfigSmall()

View File

@@ -13,8 +13,7 @@ func init() { gob.Register(new(spDBusOp)) }
// spDBusOp maintains an xdg-dbus-proxy instance for the container. // spDBusOp maintains an xdg-dbus-proxy instance for the container.
type spDBusOp struct { type spDBusOp struct {
// Whether to bind the system bus socket. // Whether to bind the system bus socket. Populated during toSystem.
// Populated during toSystem.
ProxySystem bool ProxySystem bool
} }
@@ -30,10 +29,10 @@ func (s *spDBusOp) toSystem(state *outcomeStateSys) error {
// downstream socket paths // downstream socket paths
sessionPath, systemPath := state.instance().Append("bus"), state.instance().Append("system_bus_socket") sessionPath, systemPath := state.instance().Append("bus"), state.instance().Append("system_bus_socket")
if err := state.sys.ProxyDBus( var sessionBus, systemBus dbus.ProxyPair
state.sessionBus, state.systemBus, sessionBus[0], systemBus[0] = state.k.dbusAddress()
sessionPath, systemPath, sessionBus[1], systemBus[1] = sessionPath.String(), systemPath.String()
); err != nil { if err := state.sys.ProxyDBus(state.sessionBus, state.systemBus, sessionBus, systemBus); err != nil {
return err return err
} }

227
internal/app/spdbus_test.go Normal file
View File

@@ -0,0 +1,227 @@
package app
import (
"maps"
"reflect"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/container/stub"
"hakurei.app/helper"
"hakurei.app/hst"
"hakurei.app/message"
"hakurei.app/system"
"hakurei.app/system/acl"
"hakurei.app/system/dbus"
)
func TestSpDBusOp(t *testing.T) {
config := hst.Template()
const instancePrefix = container.Nonexistent + "/tmp/hakurei.0/" + wantAutoEtcPrefix
checkOpBehaviour(t, []opBehaviourTestCase{
{"not enabled", func(bool, bool) outcomeOp {
return new(spDBusOp)
}, func() *hst.Config {
c := hst.Template()
*c.Enablements = 0
return c
}, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
{"invalid", func(bool, bool) outcomeOp {
return new(spDBusOp)
}, func() *hst.Config {
c := hst.Template()
c.SessionBus.Talk[0] += "\x00"
c.SystemBus = nil
return c
}, nil, []stub.Call{
call("dbusAddress", stub.ExpectArgs{}, [2]string{
"unix:path=/run/user/1000/bus",
"unix:path=/var/run/dbus/system_bus_socket",
}, nil),
}, nil, func(t *testing.T, state *outcomeStateSys) {
if want := m(instancePrefix); !reflect.DeepEqual(state.sharePath, want) {
t.Errorf("outcomeStateSys: sharePath = %v, want %v", state.sharePath, want)
}
}, &system.OpError{
Op: "dbus",
Err: syscall.EINVAL,
Msg: "message bus proxy configuration contains NUL byte",
Revert: false,
}, nil, nil, nil, nil, nil},
{"success default", func(bool, bool) outcomeOp {
return new(spDBusOp)
}, func() *hst.Config {
c := hst.Template()
c.SessionBus, c.SystemBus = nil, nil
return c
}, nil, []stub.Call{
call("dbusAddress", stub.ExpectArgs{}, [2]string{
"unix:path=/run/user/1000/bus",
"unix:path=/var/run/dbus/system_bus_socket",
}, nil),
call("isVerbose", stub.ExpectArgs{}, true, nil),
call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{
"unix:path=/run/user/1000/bus",
instancePrefix + "/bus",
"--filter",
"--talk=org.freedesktop.DBus",
"--talk=org.freedesktop.Notifications",
"--own=org.chromium.Chromium.*",
"--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"--call=org.freedesktop.portal.*=*",
"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
}}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs(
"unix:path=/run/user/1000/bus",
instancePrefix+"/bus",
"--filter",
"--talk=org.freedesktop.DBus",
"--talk=org.freedesktop.Notifications",
"--own=org.chromium.Chromium.*",
"--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"--call=org.freedesktop.portal.*=*",
"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
)}}, nil, nil),
}, func() *system.I {
sys := system.New(panicMsgContext{}, message.NewMsg(nil), checkExpectUid)
sys.Ephemeral(system.Process, m(instancePrefix), 0711)
if err := sys.ProxyDBus(
dbus.NewConfig(config.ID, true, true), nil,
dbus.ProxyPair{"unix:path=/run/user/1000/bus", instancePrefix + "/bus"},
dbus.ProxyPair{"unix:path=/var/run/dbus/system_bus_socket", instancePrefix + "/system_bus_socket"},
); err != nil {
t.Fatalf("cannot prepare sys: %v", err)
}
sys.UpdatePerm(m(instancePrefix+"/bus"), acl.Read, acl.Write)
return sys
}(), func(t *testing.T, state *outcomeStateSys) {
if want := m(instancePrefix); !reflect.DeepEqual(state.sharePath, want) {
t.Errorf("outcomeStateSys: sharePath = %v, want %v", state.sharePath, want)
}
}, nil, func(state *outcomeStateParams) {
state.params.Ops = new(container.Ops)
// emulates spRuntimeOp
state.runtimeDir = m("/run/user/1000")
}, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
Bind(m(instancePrefix+"/bus"),
m("/run/user/1000/bus"), 0),
}, func(t *testing.T, state *outcomeStateParams) {
wantEnv := map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus",
}
maps.Copy(wantEnv, config.Container.Env)
if !maps.Equal(state.env, wantEnv) {
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
}
}, nil},
{"success", func(isShim, _ bool) outcomeOp {
if !isShim {
return new(spDBusOp)
}
return &spDBusOp{ProxySystem: true}
}, hst.Template, nil, []stub.Call{
call("dbusAddress", stub.ExpectArgs{}, [2]string{
"unix:path=/run/user/1000/bus",
"unix:path=/var/run/dbus/system_bus_socket",
}, nil),
call("isVerbose", stub.ExpectArgs{}, true, nil),
call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{
"unix:path=/run/user/1000/bus",
instancePrefix + "/bus",
"--filter",
"--talk=org.freedesktop.Notifications",
"--talk=org.freedesktop.FileManager1",
"--talk=org.freedesktop.ScreenSaver",
"--talk=org.freedesktop.secrets",
"--talk=org.kde.kwalletd5",
"--talk=org.kde.kwalletd6",
"--talk=org.gnome.SessionManager",
"--own=org.chromium.Chromium.*",
"--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"--own=org.mpris.MediaPlayer2.chromium.*",
"--call=org.freedesktop.portal.*=*",
"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
}}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"system bus proxy:", []string{
"unix:path=/var/run/dbus/system_bus_socket",
instancePrefix + "/system_bus_socket",
"--filter",
"--talk=org.bluez",
"--talk=org.freedesktop.Avahi",
"--talk=org.freedesktop.UPower",
}}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs(
"unix:path=/run/user/1000/bus",
instancePrefix+"/bus",
"--filter",
"--talk=org.freedesktop.Notifications",
"--talk=org.freedesktop.FileManager1",
"--talk=org.freedesktop.ScreenSaver",
"--talk=org.freedesktop.secrets",
"--talk=org.kde.kwalletd5",
"--talk=org.kde.kwalletd6",
"--talk=org.gnome.SessionManager",
"--own=org.chromium.Chromium.*",
"--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"--own=org.mpris.MediaPlayer2.chromium.*",
"--call=org.freedesktop.portal.*=*",
"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
"unix:path=/var/run/dbus/system_bus_socket",
instancePrefix+"/system_bus_socket",
"--filter",
"--talk=org.bluez",
"--talk=org.freedesktop.Avahi",
"--talk=org.freedesktop.UPower",
)}}, nil, nil),
}, func() *system.I {
sys := system.New(panicMsgContext{}, message.NewMsg(nil), checkExpectUid)
sys.Ephemeral(system.Process, m(instancePrefix), 0711)
if err := sys.ProxyDBus(
config.SessionBus, config.SystemBus,
dbus.ProxyPair{"unix:path=/run/user/1000/bus", instancePrefix + "/bus"},
dbus.ProxyPair{"unix:path=/var/run/dbus/system_bus_socket", instancePrefix + "/system_bus_socket"},
); err != nil {
t.Fatalf("cannot prepare sys: %v", err)
}
sys.UpdatePerm(m(instancePrefix+"/bus"), acl.Read, acl.Write).
UpdatePerm(m(instancePrefix+"/system_bus_socket"), acl.Read, acl.Write)
return sys
}(), func(t *testing.T, state *outcomeStateSys) {
if want := m(instancePrefix); !reflect.DeepEqual(state.sharePath, want) {
t.Errorf("outcomeStateSys: sharePath = %v, want %v", state.sharePath, want)
}
}, nil, func(state *outcomeStateParams) {
state.params.Ops = new(container.Ops)
// emulates spRuntimeOp
state.runtimeDir = m("/run/user/1000")
}, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
Bind(m(instancePrefix+"/bus"),
m("/run/user/1000/bus"), 0).
Bind(m(instancePrefix+"/system_bus_socket"),
m("/var/run/dbus/system_bus_socket"), 0),
}, func(t *testing.T, state *outcomeStateParams) {
wantEnv := map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus",
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/var/run/dbus/system_bus_socket",
}
maps.Copy(wantEnv, config.Container.Env)
if !maps.Equal(state.env, wantEnv) {
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
}
}, nil},
})
}

View File

@@ -21,8 +21,9 @@ type spFinalOp struct{}
func (s spFinalOp) toSystem(state *outcomeStateSys) error { func (s spFinalOp) toSystem(state *outcomeStateSys) error {
// append ExtraPerms last // append ExtraPerms last
for _, p := range state.extraPerms { for i := range state.extraPerms {
if p == nil || p.Path == nil { p := &state.extraPerms[i]
if p.Path == nil {
continue continue
} }

View File

@@ -54,7 +54,7 @@ func (s *spX11Op) toSystem(state *outcomeStateSys) error {
} }
} else { } else {
state.sys.UpdatePermType(hst.EX11, socketPath, acl.Read, acl.Write, acl.Execute) state.sys.UpdatePermType(hst.EX11, socketPath, acl.Read, acl.Write, acl.Execute)
if !state.Container.HostAbstract { if state.Container.Flags&hst.FHostAbstract == 0 {
s.Display = "unix:" + socketPath.String() s.Display = "unix:" + socketPath.String()
} }
} }

View File

@@ -12,18 +12,19 @@ import (
"syscall" "syscall"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
var ( // ErrDBusConfig is returned when a required [hst.BusConfig] argument is nil.
ErrDBusConfig = errors.New("dbus config not supplied") var ErrDBusConfig = errors.New("dbus config not supplied")
)
// MustProxyDBus calls ProxyDBus and panics if an error is returned. // MustProxyDBus calls ProxyDBus and panics if an error is returned.
func (sys *I) MustProxyDBus(sessionPath *check.Absolute, session *hst.BusConfig, systemPath *check.Absolute, system *hst.BusConfig) *I { func (sys *I) MustProxyDBus(
if err := sys.ProxyDBus(session, system, sessionPath, systemPath); err != nil { session, system *hst.BusConfig,
sessionBus, systemBus dbus.ProxyPair,
) *I {
if err := sys.ProxyDBus(session, system, sessionBus, systemBus); err != nil {
panic(err.Error()) panic(err.Error())
} else { } else {
return sys return sys
@@ -32,7 +33,10 @@ func (sys *I) MustProxyDBus(sessionPath *check.Absolute, session *hst.BusConfig,
// ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via [dbus] and terminates it on revert. // ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via [dbus] and terminates it on revert.
// This [Op] is always [Process] scoped. // This [Op] is always [Process] scoped.
func (sys *I) ProxyDBus(session, system *hst.BusConfig, sessionPath, systemPath *check.Absolute) error { func (sys *I) ProxyDBus(
session, system *hst.BusConfig,
sessionBus, systemBus dbus.ProxyPair,
) error {
d := new(dbusProxyOp) d := new(dbusProxyOp)
// session bus is required as otherwise this is effectively a very expensive noop // session bus is required as otherwise this is effectively a very expensive noop
@@ -44,9 +48,6 @@ func (sys *I) ProxyDBus(session, system *hst.BusConfig, sessionPath, systemPath
// system bus is optional // system bus is optional
d.system = system != nil d.system = system != nil
var sessionBus, systemBus dbus.ProxyPair
sessionBus[0], systemBus[0] = sys.dbusAddress()
sessionBus[1], systemBus[1] = sessionPath.String(), systemPath.String()
d.out = &linePrefixWriter{println: log.Println, prefix: "(dbus) ", buf: new(strings.Builder)} d.out = &linePrefixWriter{println: log.Println, prefix: "(dbus) ", buf: new(strings.Builder)}
if final, err := sys.dbusFinalise(sessionBus, systemBus, session, system); err != nil { if final, err := sys.dbusFinalise(sessionBus, systemBus, session, system); err != nil {
if errors.Is(err, syscall.EINVAL) { if errors.Is(err, syscall.EINVAL) {

View File

@@ -8,7 +8,8 @@ import (
) )
const ( const (
/*SessionBusAddress is the name of the environment variable where the address of the login session message bus is given in. /*
SessionBusAddress is the name of the environment variable where the address of the login session message bus is given in.
If that variable is not set, applications may also try to read the address from the X Window System root window property _DBUS_SESSION_BUS_ADDRESS. If that variable is not set, applications may also try to read the address from the X Window System root window property _DBUS_SESSION_BUS_ADDRESS.
The root window property must have type STRING. The environment variable should have precedence over the root window property. The root window property must have type STRING. The environment variable should have precedence over the root window property.
@@ -22,13 +23,16 @@ const (
Therefore, it is recommended that applications do not try to make this determination for their functionality purposes, and instead they should attempt to start the server. Therefore, it is recommended that applications do not try to make this determination for their functionality purposes, and instead they should attempt to start the server.
This package diverges from the specification, as the caller is unlikely to be an X client, or be in a position to autolaunch a dbus server. This package diverges from the specification, as the caller is unlikely to be an X client, or be in a position to autolaunch a dbus server.
So a fallback address with a socket located in the well-known default XDG_RUNTIME_DIR formatting is used.*/ So a fallback address with a socket located in the well-known default XDG_RUNTIME_DIR formatting is used.
*/
SessionBusAddress = "DBUS_SESSION_BUS_ADDRESS" SessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
/*SystemBusAddress is the name of the environment variable where the address of the system message bus is given in. /*
SystemBusAddress is the name of the environment variable where the address of the system message bus is given in.
If that variable is not set, applications should try to connect to the well-known address unix:path=/var/run/dbus/system_bus_socket. If that variable is not set, applications should try to connect to the well-known address unix:path=/var/run/dbus/system_bus_socket.
Implementations of the well-known system bus should listen on an address that will result in that connection being successful.*/ Implementations of the well-known system bus should listen on an address that will result in that connection being successful.
*/
SystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS" SystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
// FallbackSystemBusAddress is used when [SystemBusAddress] is not set. // FallbackSystemBusAddress is used when [SystemBusAddress] is not set.
@@ -36,28 +40,30 @@ const (
) )
var ( var (
addresses [2]string address [2]string
addressOnce sync.Once addressOnce sync.Once
) )
// Address returns the session and system bus addresses copied from environment,
// or appropriate fallback values if they are not set.
func Address() (session, system string) { func Address() (session, system string) {
addressOnce.Do(func() { addressOnce.Do(func() {
// resolve upstream session bus address // resolve upstream session bus address
if addr, ok := os.LookupEnv(SessionBusAddress); !ok { if addr, ok := os.LookupEnv(SessionBusAddress); !ok {
// fall back to default format // fall back to default format
addresses[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid()) address[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid())
} else { } else {
addresses[0] = addr address[0] = addr
} }
// resolve upstream system bus address // resolve upstream system bus address
if addr, ok := os.LookupEnv(SystemBusAddress); !ok { if addr, ok := os.LookupEnv(SystemBusAddress); !ok {
// fall back to default hardcoded value // fall back to default hardcoded value
addresses[1] = FallbackSystemBusAddress address[1] = FallbackSystemBusAddress
} else { } else {
addresses[1] = addr address[1] = addr
} }
}) })
return addresses[0], addresses[1] return address[0], address[1]
} }

View File

@@ -78,7 +78,7 @@ func Finalise(sessionBus, systemBus ProxyPair, session, system *hst.BusConfig) (
final = &Final{Session: sessionBus, System: systemBus} final = &Final{Session: sessionBus, System: systemBus}
final.WriterTo, err = helper.NewCheckedArgs(args) final.WriterTo, err = helper.NewCheckedArgs(args...)
if err != nil { if err != nil {
return return
} }

View File

@@ -85,7 +85,7 @@ func TestDBusProxyOp(t *testing.T) {
Op: "dbus", Err: ErrDBusConfig, Op: "dbus", Err: ErrDBusConfig,
Msg: "attempted to create message bus proxy args without session bus config", Msg: "attempted to create message bus proxy args without session bus config",
} }
if err := sys.ProxyDBus(nil, new(hst.BusConfig), nil, nil); !reflect.DeepEqual(err, wantErr) { if err := sys.ProxyDBus(nil, new(hst.BusConfig), dbus.ProxyPair{}, dbus.ProxyPair{}); !reflect.DeepEqual(err, wantErr) {
t.Errorf("ProxyDBus: error = %v, want %v", err, wantErr) t.Errorf("ProxyDBus: error = %v, want %v", err, wantErr)
} }
}, nil, stub.Expect{}}, }, nil, stub.Expect{}},
@@ -99,15 +99,20 @@ func TestDBusProxyOp(t *testing.T) {
}() }()
sys.MustProxyDBus( sys.MustProxyDBus(
m("/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"), &hst.BusConfig{ &hst.BusConfig{
// use impossible value here as an implicit assert that it goes through the stub // use impossible value here as an implicit assert that it goes through the stub
Talk: []string{"session\x00"}, Filter: true, Talk: []string{"session\x00"}, Filter: true,
}, m("/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"), &hst.BusConfig{ }, &hst.BusConfig{
// use impossible value here as an implicit assert that it goes through the stub // use impossible value here as an implicit assert that it goes through the stub
Talk: []string{"system\x00"}, Filter: true, Talk: []string{"system\x00"}, Filter: true,
}, dbus.ProxyPair{
"unix:path=/run/user/1000/bus",
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus",
}, dbus.ProxyPair{
"unix:path=/run/dbus/system_bus_socket",
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket",
}) })
}, nil, stub.Expect{Calls: []stub.Call{ }, nil, stub.Expect{Calls: []stub.Call{
call("dbusAddress", stub.ExpectArgs{}, [2]string{"unix:path=/run/user/1000/bus", "unix:path=/run/dbus/system_bus_socket"}, nil),
call("dbusFinalise", stub.ExpectArgs{ call("dbusFinalise", stub.ExpectArgs{
dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"}, dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"},
dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"}, dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"},
@@ -128,13 +133,16 @@ func TestDBusProxyOp(t *testing.T) {
}, &hst.BusConfig{ }, &hst.BusConfig{
// use impossible value here as an implicit assert that it goes through the stub // use impossible value here as an implicit assert that it goes through the stub
Talk: []string{"system\x00"}, Filter: true, Talk: []string{"system\x00"}, Filter: true,
}, }, dbus.ProxyPair{
m("/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"), "unix:path=/run/user/1000/bus",
m("/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket")); !reflect.DeepEqual(err, wantErr) { "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus",
}, dbus.ProxyPair{
"unix:path=/run/dbus/system_bus_socket",
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket",
}); !reflect.DeepEqual(err, wantErr) {
t.Errorf("ProxyDBus: error = %v", err) t.Errorf("ProxyDBus: error = %v", err)
} }
}, nil, stub.Expect{Calls: []stub.Call{ }, nil, stub.Expect{Calls: []stub.Call{
call("dbusAddress", stub.ExpectArgs{}, [2]string{"unix:path=/run/user/1000/bus", "unix:path=/run/dbus/system_bus_socket"}, nil),
call("dbusFinalise", stub.ExpectArgs{ call("dbusFinalise", stub.ExpectArgs{
dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"}, dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"},
dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"}, dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"},
@@ -145,12 +153,18 @@ func TestDBusProxyOp(t *testing.T) {
{"full", 0xcafebabe, func(_ *testing.T, sys *I) { {"full", 0xcafebabe, func(_ *testing.T, sys *I) {
sys.MustProxyDBus( sys.MustProxyDBus(
m("/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"), &hst.BusConfig{ &hst.BusConfig{
// use impossible value here as an implicit assert that it goes through the stub // use impossible value here as an implicit assert that it goes through the stub
Talk: []string{"session\x00"}, Filter: true, Talk: []string{"session\x00"}, Filter: true,
}, m("/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"), &hst.BusConfig{ }, &hst.BusConfig{
// use impossible value here as an implicit assert that it goes through the stub // use impossible value here as an implicit assert that it goes through the stub
Talk: []string{"system\x00"}, Filter: true, Talk: []string{"system\x00"}, Filter: true,
}, dbus.ProxyPair{
"unix:path=/run/user/1000/bus",
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus",
}, dbus.ProxyPair{
"unix:path=/run/dbus/system_bus_socket",
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket",
}) })
}, []Op{ }, []Op{
&dbusProxyOp{ &dbusProxyOp{
@@ -158,7 +172,6 @@ func TestDBusProxyOp(t *testing.T) {
system: true, system: true,
}, },
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("dbusAddress", stub.ExpectArgs{}, [2]string{"unix:path=/run/user/1000/bus", "unix:path=/run/dbus/system_bus_socket"}, nil),
call("dbusFinalise", stub.ExpectArgs{ call("dbusFinalise", stub.ExpectArgs{
dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"}, dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"},
dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"}, dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"},
@@ -168,7 +181,7 @@ func TestDBusProxyOp(t *testing.T) {
call("isVerbose", stub.ExpectArgs{}, true, nil), call("isVerbose", stub.ExpectArgs{}, true, nil),
call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "--filter", "--talk=session\x00"}}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "--filter", "--talk=session\x00"}}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"system bus proxy:", []string{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "--filter", "--talk=system\x00"}}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{"system bus proxy:", []string{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "--filter", "--talk=system\x00"}}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs([]string{"unique", "value", "0", "injected", "by", "the", "test", "suite"})}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs("unique", "value", "0", "injected", "by", "the", "test", "suite")}}, nil, nil),
}}}, }}},
}) })
@@ -183,10 +196,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: false, }, system: false,
}, &dbusProxyOp{final: &dbus.Final{ }, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
@@ -195,10 +208,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, false}, }, false},
@@ -209,10 +222,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1001/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1001/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, &dbusProxyOp{final: &dbus.Final{ }, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
@@ -221,10 +234,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, false}, }, false},
@@ -235,10 +248,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket\x00"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket\x00"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, &dbusProxyOp{final: &dbus.Final{ }, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
@@ -247,10 +260,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, false}, }, false},
@@ -261,10 +274,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1001/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1001/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, &dbusProxyOp{final: &dbus.Final{ }, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
@@ -273,10 +286,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, false}, }, false},
@@ -287,10 +300,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, &dbusProxyOp{final: &dbus.Final{ }, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
@@ -299,10 +312,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, false}, }, false},
@@ -313,10 +326,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, &dbusProxyOp{final: &dbus.Final{ }, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
@@ -325,10 +338,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, false}, }, false},
@@ -339,10 +352,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, &dbusProxyOp{final: &dbus.Final{ }, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
@@ -351,10 +364,10 @@ func TestDBusProxyOp(t *testing.T) {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{ WriterTo: helper.MustNewCheckedArgs(
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}), ),
}, system: true, }, system: true,
}, true}, }, true},
}) })
@@ -374,7 +387,7 @@ func dbusNewFinalSample(v int) *dbus.Final {
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/dbus/system_bus_socket"}}}}, SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{"unique", "value", strconv.Itoa(v), "injected", "by", "the", "test", "suite"}), WriterTo: helper.MustNewCheckedArgs("unique", "value", strconv.Itoa(v), "injected", "by", "the", "test", "suite"),
} }
} }

View File

@@ -48,8 +48,6 @@ type syscallDispatcher interface {
// xcbChangeHosts provides [xcb.ChangeHosts]. // xcbChangeHosts provides [xcb.ChangeHosts].
xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error
// dbusAddress provides [dbus.Address].
dbusAddress() (session, system string)
// dbusFinalise provides [dbus.Finalise]. // dbusFinalise provides [dbus.Finalise].
dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, system *hst.BusConfig) (final *dbus.Final, err error) dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, system *hst.BusConfig) (final *dbus.Final, err error)
// dbusProxyStart provides the Start method of [dbus.Proxy]. // dbusProxyStart provides the Start method of [dbus.Proxy].
@@ -82,10 +80,6 @@ func (k direct) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address str
return xcb.ChangeHosts(mode, family, address) return xcb.ChangeHosts(mode, family, address)
} }
func (k direct) dbusAddress() (session, system string) {
return dbus.Address()
}
func (k direct) dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, system *hst.BusConfig) (final *dbus.Final, err error) { func (k direct) dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, system *hst.BusConfig) (final *dbus.Final, err error) {
return dbus.Finalise(sessionBus, systemBus, session, system) return dbus.Finalise(sessionBus, systemBus, session, system)
} }

View File

@@ -276,12 +276,6 @@ func (k *kstub) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address str
stub.CheckArg(k.Stub, "address", address, 2)) stub.CheckArg(k.Stub, "address", address, 2))
} }
func (k *kstub) dbusAddress() (session, system string) {
k.Helper()
ret := k.Expects("dbusAddress").Ret.([2]string)
return ret[0], ret[1]
}
func (k *kstub) dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, system *hst.BusConfig) (final *dbus.Final, err error) { func (k *kstub) dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, system *hst.BusConfig) (final *dbus.Final, err error) {
k.Helper() k.Helper()
expect := k.Expects("dbusFinalise") expect := k.Expects("dbusFinalise")

View File

@@ -214,8 +214,8 @@ in
(ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000004,gid=1000004") (ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
(ent "/" "/dev" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/" "/dev" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666") (ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/shm" "rw,nosuid,nodev" "tmpfs" "tmpfs" "rw") (ent "/" ignore ignore ignore ignore ignore) # not deterministic
(ent "/" ignore ignore ignore ignore ignore) # order not deterministic (ent "/" ignore ignore ignore ignore ignore)
(ent "/" ignore ignore ignore ignore ignore) (ent "/" ignore ignore ignore ignore ignore)
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000004,gid=1000004") (ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000004,gid=1000004")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")