Compare commits

..

2 Commits

Author SHA1 Message Date
22d577ab49
test/sandbox: do not discard stderr getting hash
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Create distribution (pull_request) Successful in 29s
Test / Sandbox (push) Successful in 45s
Test / Hakurei (push) Successful in 47s
Test / Hakurei (race detector) (push) Successful in 48s
Test / Hpkg (push) Successful in 46s
Test / Sandbox (pull_request) Successful in 45s
Test / Hakurei (pull_request) Successful in 49s
Test / Hakurei (race detector) (pull_request) Successful in 49s
Test / Hpkg (pull_request) Successful in 46s
Test / Sandbox (race detector) (pull_request) Successful in 1m16s
Test / Sandbox (race detector) (push) Successful in 1m25s
Test / Flake checks (pull_request) Successful in 1m35s
Test / Flake checks (push) Successful in 1m34s
This is the first hakurei run in the test, if the container outright fails to start this is often where it happens, so throwing away the output is very unhelpful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-18 11:36:13 +09:00
83a1c75f1a
app: set up acl on X11 socket
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m22s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hpkg (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Flake checks (push) Successful in 1m38s
The socket is typically owned by the priv-user, and inaccessible by the target user, so just allowing access to the directory is not enough. This change fixes this oversight and add checks that will also be useful for merging #1.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-18 11:30:58 +09:00
18 changed files with 26 additions and 137 deletions

View File

@ -28,8 +28,6 @@ type appInfo struct {
// passed through to [hst.Config]
Net bool `json:"net,omitempty"`
// passed through to [hst.Config]
ScopeAbstract bool `json:"scope_abstract,omitempty"`
// passed through to [hst.Config]
Device bool `json:"dev,omitempty"`
// passed through to [hst.Config]
Tty bool `json:"tty,omitempty"`

View File

@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"runtime"
@ -15,7 +14,6 @@ import (
. "syscall"
"time"
"hakurei.app/container/landlock"
"hakurei.app/container/seccomp"
)
@ -94,8 +92,6 @@ type (
RetainSession bool
// Do not [syscall.CLONE_NEWNET].
HostNet bool
// Scope abstract UNIX domain sockets using LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET.
ScopeAbstract bool
// Retain CAP_SYS_ADMIN.
Privileged bool
}
@ -183,12 +179,6 @@ func (p *Container) Start() error {
p.wait = make(chan struct{})
done <- func() error { // setup depending on per-thread state must happen here
if p.ScopeAbstract {
if err := landlock.ScopeAbstract(); err != nil {
log.Fatalf("could not scope abstract unix sockets: %v", err)
}
}
msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil {
return msg.WrapErr(err, err.Error())

View File

@ -1,55 +0,0 @@
package landlock
/*
#include <linux/landlock.h>
#include <sys/syscall.h>
*/
import "C"
import (
"fmt"
"syscall"
"unsafe"
)
const (
LANDLOCK_CREATE_RULESET_VERSION = C.LANDLOCK_CREATE_RULESET_VERSION
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET = C.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
SYS_LANDLOCK_CREATE_RULESET = C.SYS_landlock_create_ruleset
SYS_LANDLOCK_RESTRICT_SELF = C.SYS_landlock_restrict_self
)
type LandlockRulesetAttr = C.struct_landlock_ruleset_attr
// ScopeAbstract calls landlock_restrict_self and must be called from a goroutine wired to an m
// with the process starting from the same goroutine.
func ScopeAbstract() error {
abi, _, err := syscall.Syscall(SYS_LANDLOCK_CREATE_RULESET, 0, 0, LANDLOCK_CREATE_RULESET_VERSION)
if err != 0 {
return fmt.Errorf("could not fetch landlock ABI: errno %v", err)
}
if abi < 6 {
return fmt.Errorf("landlock ABI must be >= 6, got %d", abi)
}
attrs := LandlockRulesetAttr{
scoped: LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
}
fd, _, err := syscall.Syscall(SYS_LANDLOCK_CREATE_RULESET, uintptr(unsafe.Pointer(&attrs)), unsafe.Sizeof(attrs), 0)
if err != 0 {
return fmt.Errorf("could not create landlock ruleset: errno %v", err)
}
defer syscall.Close(int(fd))
r, _, err := syscall.Syscall(SYS_LANDLOCK_RESTRICT_SELF, fd, 0, 0)
if r != 0 {
return fmt.Errorf("could not restrict self via landlock: errno %v", err)
}
return nil
}

View File

@ -79,8 +79,6 @@ type (
Userns bool `json:"userns,omitempty"`
// share host net namespace
Net bool `json:"net,omitempty"`
// disallow accessing abstract UNIX domain sockets created outside the container
ScopeAbstract bool `json:"scope_abstract,omitempty"`
// allow dangerous terminal I/O
Tty bool `json:"tty,omitempty"`
// allow multiarch

View File

@ -33,7 +33,6 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
SeccompPresets: s.SeccompPresets,
RetainSession: s.Tty,
HostNet: s.Net,
ScopeAbstract: s.ScopeAbstract,
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
// this behaviour is implemented in the shim

View File

@ -387,10 +387,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
return hlog.WrapErr(ErrXDisplay,
"DISPLAY is not set")
} else {
seal.sys.ChangeHosts("#" + seal.user.uid.String())
seal.env[display] = d
socketDir := container.AbsFHSTmp.Append(".X11-unix")
seal.container.Bind(socketDir, socketDir, 0)
// the socket file at `/tmp/.X11-unix/X%d` is typically owned by the priv user
// and not accessible by the target user
@ -405,8 +402,20 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
}
}
if socketPath != nil {
seal.sys.UpdatePermTypeOptional(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute)
if _, err := sys.Stat(socketPath.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access X11 socket %q:", socketPath))
}
} else {
seal.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute)
d = "unix:" + socketPath.String()
}
}
seal.sys.ChangeHosts("#" + seal.user.uid.String())
seal.env[display] = d
seal.container.Bind(socketDir, socketDir, 0)
}
}

View File

@ -137,7 +137,6 @@ in
multiarch
env
;
scope_abstract = app.scopeAbstract;
map_real_uid = app.mapRealUid;
filesystem =

View File

@ -572,28 +572,6 @@ boolean
*Example:*
` true `
## environment\.hakurei\.apps\.\<name>\.scopeAbstract
Whether to restrict abstract UNIX domain socket access\.
*Type:*
boolean
*Default:*
` true `
*Example:*
` true `

View File

@ -182,9 +182,6 @@ in
net = mkEnableOption "network access" // {
default = true;
};
scopeAbstract = mkEnableOption "abstract unix domain socket access" // {
default = true;
};
nix = mkEnableOption "nix daemon access";
mapRealUid = mkEnableOption "mapping to priv-user uid";

View File

@ -21,17 +21,7 @@ func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &ACL{et, path, perms, false})
return sys
}
// UpdatePermTypeOptional appends an acl update Op that silently continues if the target does not exist.
func (sys *I) UpdatePermTypeOptional(et Enablement, path string, perms ...acl.Perm) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &ACL{et, path, perms, true})
sys.ops = append(sys.ops, &ACL{et, path, perms})
return sys
}
@ -40,24 +30,14 @@ type ACL struct {
et Enablement
path string
perms acl.Perms
// since revert operations are cross-process, the success of apply must not affect the outcome of revert
skipNotExist bool
}
func (a *ACL) Type() Enablement { return a.et }
func (a *ACL) apply(sys *I) error {
msg.Verbose("applying ACL", a)
if err := acl.Update(a.path, sys.uid, a.perms...); err != nil {
if !a.skipNotExist || !os.IsNotExist(err) {
return wrapErrSuffix(err,
fmt.Sprintf("cannot apply ACL entry to %q:", a.path))
}
msg.Verbosef("path %q does not exist", a.path)
return nil
}
return nil
return wrapErrSuffix(acl.Update(a.path, sys.uid, a.perms...),
fmt.Sprintf("cannot apply ACL entry to %q:", a.path))
}
func (a *ACL) revert(sys *I, ec *Criteria) error {

View File

@ -20,7 +20,7 @@ func TestUpdatePerm(t *testing.T) {
t.Run(tc.path+permSubTestSuffix(tc.perms), func(t *testing.T) {
sys := New(150)
sys.UpdatePerm(tc.path, tc.perms...)
(&tcOp{Process, tc.path}).test(t, sys.ops, []Op{&ACL{Process, tc.path, tc.perms, false}}, "UpdatePerm")
(&tcOp{Process, tc.path}).test(t, sys.ops, []Op{&ACL{Process, tc.path, tc.perms}}, "UpdatePerm")
})
}
}
@ -42,7 +42,7 @@ func TestUpdatePermType(t *testing.T) {
t.Run(tc.path+"_"+TypeString(tc.et)+permSubTestSuffix(tc.perms), func(t *testing.T) {
sys := New(150)
sys.UpdatePermType(tc.et, tc.path, tc.perms...)
tc.test(t, sys.ops, []Op{&ACL{tc.et, tc.path, tc.perms, false}}, "UpdatePermType")
tc.test(t, sys.ops, []Op{&ACL{tc.et, tc.path, tc.perms}}, "UpdatePermType")
})
}
}

View File

@ -64,10 +64,6 @@ func (p *Proxy) Start() error {
argF, func(z *container.Container) {
z.SeccompFlags |= seccomp.AllowMultiarch
z.SeccompPresets |= seccomp.PresetStrict
// xdg-dbus-proxy requires host abstract UNIX domain socket access
z.ScopeAbstract = false
z.Hostname = "hakurei-dbus"
if p.output != nil {
z.Stdout, z.Stderr = p.output, p.output

View File

@ -36,7 +36,7 @@ in
want = {
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"DISPLAY=:0"
"DISPLAY=unix:/tmp/.X11-unix/X0"
"HOME=/var/lib/hakurei/u0/a4"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
"SHELL=/run/current-system/sw/bin/bash"
@ -243,7 +243,7 @@ in
seccomp = true;
try_socket = "/tmp/.X11-unix/X0";
socket_abstract = false;
socket_abstract = true;
socket_pathname = true;
};
}

View File

@ -269,7 +269,7 @@ in
seccomp = true;
try_socket = "/tmp/.X11-unix/X0";
socket_abstract = false;
socket_abstract = true;
socket_pathname = false;
};
}

View File

@ -264,7 +264,7 @@ in
seccomp = true;
try_socket = "/tmp/.X11-unix/X0";
socket_abstract = false;
socket_abstract = true;
socket_pathname = false;
};
}

View File

@ -262,7 +262,7 @@ in
seccomp = true;
try_socket = "/tmp/.X11-unix/X0";
socket_abstract = false;
socket_abstract = true;
socket_pathname = false;
};
}

View File

@ -45,7 +45,7 @@ in
want = {
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"DISPLAY=:0"
"DISPLAY=unix:/tmp/.X11-unix/X0"
"HOME=/var/lib/hakurei/u0/a2"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
"SHELL=/run/current-system/sw/bin/bash"
@ -275,7 +275,7 @@ in
seccomp = true;
try_socket = "/tmp/.X11-unix/X0";
socket_abstract = false;
socket_abstract = true;
socket_pathname = true;
};
}

View File

@ -27,7 +27,7 @@ def swaymsg(command: str = "", succeed=True, type="command"):
def check_filter(check_offset, name, pname):
pid = int(machine.wait_until_succeeds(f"pgrep -U {1000000+check_offset} -x {pname}", timeout=15))
hash = machine.succeed(f"sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 check-sandbox-{name} hash 2>/dev/null")
hash = machine.succeed(f"sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 check-sandbox-{name} hash")
print(machine.succeed(f"hakurei-test -s {hash} filter {pid}"))