Compare commits

..

2 Commits

Author SHA1 Message Date
c9eeafbbf0
container: optionally isolate host abstract UNIX domain sockets via landlock
Some checks failed
Test / Create distribution (pull_request) Failing after 32s
Test / Sandbox (pull_request) Failing after 51s
Test / Sandbox (race detector) (pull_request) Failing after 54s
Test / Hpkg (pull_request) Failing after 56s
Test / Hakurei (pull_request) Failing after 1m9s
Test / Hakurei (race detector) (push) Failing after 1m0s
Test / Hakurei (push) Failing after 1m11s
Test / Hakurei (race detector) (pull_request) Failing after 1m18s
Test / Flake checks (pull_request) Has been skipped
Test / Create distribution (push) Failing after 30s
Test / Sandbox (push) Failing after 49s
Test / Hpkg (push) Failing after 48s
Test / Sandbox (race detector) (push) Failing after 51s
Test / Flake checks (push) Has been skipped
2025-08-18 11:50:05 +09:00
2f1d42c8dd
app: set up acl on X11 socket
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>

# Conflicts:
#	test/sandbox/case/device.nix
#	test/sandbox/case/tty.nix
2025-08-18 11:49:49 +09:00
14 changed files with 54 additions and 53 deletions

View File

@ -92,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
}

View File

@ -13,7 +13,6 @@ import (
. "syscall"
"time"
"hakurei.app/container/landlock"
"hakurei.app/container/seccomp"
)
@ -261,12 +260,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
msg.Verbose("syscall filter not configured")
}
if params.ScopeAbstract {
if err := landlock.ScopeAbstract(); err != nil {
log.Fatalf("could not scope abstract unix sockets: %v", err)
}
}
extraFiles := make([]*os.File, params.Count)
for i := range extraFiles {
// setup fd is placed before all extra files

View File

@ -1,14 +0,0 @@
#include <errno.h>
#include <linux/landlock.h>
#include <sys/psx_syscall.h>
#include <sys/syscall.h>
#include "landlock-helper.h"
int hakurei_scope_abstract_unix_sockets(int* p_errno, int fd) {
int res = psx_syscall3(SYS_landlock_restrict_self, fd, 0, 0);
*p_errno = errno;
return res;
}

View File

@ -1,3 +0,0 @@
#pragma once
int hakurei_scope_abstract_unix_sockets(int* p_errno, int fd);

View File

@ -1,12 +1,8 @@
package landlock
/*
#cgo linux pkg-config: --static libpsx
#include <linux/landlock.h>
#include <sys/syscall.h>
#include "landlock-helper.h"
*/
import "C"
@ -21,10 +17,13 @@ const (
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)
@ -48,10 +47,9 @@ func ScopeAbstract() error {
defer syscall.Close(int(fd))
var errno C.int
if rv := C.hakurei_scope_abstract_unix_sockets(&errno, C.int(fd)); rv != 0 {
return fmt.Errorf("could not restrict self via landlock: errno %v", errno)
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

@ -416,6 +416,22 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
seal.sys.ChangeHosts("#" + seal.user.uid.String())
seal.env[display] = d
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
var socketPath *container.Absolute
if len(d) > 1 && d[0] == ':' { // `:%d`
if n, err := strconv.Atoi(d[1:]); err == nil && n >= 0 {
socketPath = socketDir.Append("X" + strconv.Itoa(n))
}
} else if len(d) > 5 && strings.HasPrefix(d, "unix:") { // `unix:%s`
if a, err := container.NewAbs(d[5:]); err == nil {
socketPath = a
}
}
if socketPath != nil {
seal.sys.UpdatePermTypeOptional(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute)
}
}
}

View File

@ -5,7 +5,6 @@
makeBinaryWrapper,
xdg-dbus-proxy,
pkg-config,
libcap,
libffi,
libseccomp,
acl,
@ -81,16 +80,10 @@ buildGoModule rec {
hsu = "/run/wrappers/bin/hsu";
};
env = {
# required by libpsx
CGO_LDFLAGS_ALLOW = "-Wl,(--no-whole-archive|--whole-archive)";
# nix build environment does not allow acls
GO_TEST_SKIP_ACL = 1;
};
env.GO_TEST_SKIP_ACL = 1;
buildInputs = [
libcap
libffi
libseccomp
acl

View File

@ -21,7 +21,17 @@ 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})
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})
return sys
}
@ -30,14 +40,24 @@ 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)
return wrapErrSuffix(acl.Update(a.path, sys.uid, a.perms...),
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
}
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}}, "UpdatePerm")
(&tcOp{Process, tc.path}).test(t, sys.ops, []Op{&ACL{Process, tc.path, tc.perms, false}}, "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}}, "UpdatePermType")
tc.test(t, sys.ops, []Op{&ACL{tc.et, tc.path, tc.perms, false}}, "UpdatePermType")
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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