container: optionally isolate host abstract UNIX domain sockets via landlock
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Create distribution (pull_request) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Sandbox (pull_request) Successful in 2m10s
Test / Hpkg (pull_request) Successful in 4m2s
Test / Hpkg (push) Successful in 4m11s
Test / Sandbox (race detector) (pull_request) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (pull_request) Successful in 4m39s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Hakurei (race detector) (pull_request) Successful in 4m57s
Test / Flake checks (pull_request) Successful in 1m25s
Test / Hakurei (push) Successful in 38s
Test / Flake checks (push) Successful in 1m23s

This commit is contained in:
Clayton Gilmer 2025-08-18 12:00:52 +09:00 committed by Ophestra
parent 69a4ab8105
commit 40028f3c03
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
14 changed files with 148 additions and 5 deletions

View File

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

View File

@ -92,6 +92,8 @@ type (
RetainSession bool RetainSession bool
// Do not [syscall.CLONE_NEWNET]. // Do not [syscall.CLONE_NEWNET].
HostNet bool HostNet bool
// Do not [LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET].
HostAbstract bool
// Retain CAP_SYS_ADMIN. // Retain CAP_SYS_ADMIN.
Privileged bool Privileged bool
} }
@ -185,6 +187,53 @@ func (p *Container) Start() error {
"prctl(PR_SET_NO_NEW_PRIVS):") "prctl(PR_SET_NO_NEW_PRIVS):")
} }
// landlock: depends on per-thread state but acts on a process group
{
scoped := LANDLOCK_SCOPE_SIGNAL
if !p.HostAbstract {
scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
}
rulesetAttr := NewRulesetAttr(scoped)
if abi, err := LandlockGetABI(); err != nil {
if p.HostAbstract {
// landlock can be skipped here as it restricts access to resources
// already covered by namespaces (pid)
goto landlockOut
}
return wrapErrSuffix(err,
"landlock does not appear to be enabled:")
} else if abi < 6 {
if p.HostAbstract {
// see above comment
goto landlockOut
}
return msg.WrapErr(ENOSYS,
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET")
} else {
msg.Verbosef("landlock abi version %d", abi)
}
msg.Verbosef("creating landlock ruleset scoped %#x", int(scoped))
if rulesetFd, err := LandlockCreateRuleset(&rulesetAttr, 0); err != nil {
return wrapErrSuffix(err,
"cannot create landlock ruleset:")
} else {
msg.Verbose("enforcing landlock ruleset")
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd)
return wrapErrSuffix(err,
"cannot enforce landlock ruleset:")
}
if err = Close(rulesetFd); err != nil {
msg.Verbosef("cannot close landlock ruleset: %v", err)
// not fatal
}
}
landlockOut:
}
msg.Verbose("starting container init") msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil { if err := p.cmd.Start(); err != nil {
return msg.WrapErr(err, err.Error()) return msg.WrapErr(err, err.Error())

59
container/landlock.go Normal file
View File

@ -0,0 +1,59 @@
package container
/*
#include <linux/landlock.h>
#include <sys/syscall.h>
*/
import "C"
import (
"syscall"
"unsafe"
"hakurei.app/container/seccomp"
)
const (
LANDLOCK_CREATE_RULESET_VERSION = C.LANDLOCK_CREATE_RULESET_VERSION
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET = C.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
LANDLOCK_SCOPE_SIGNAL = C.LANDLOCK_SCOPE_SIGNAL
)
type RulesetAttr = C.struct_landlock_ruleset_attr
func NewRulesetAttr(scoped int) RulesetAttr { return RulesetAttr{scoped: C.__u64(scoped)} }
/* TODO: remove everything above this */
func LandlockCreateRuleset(rulesetAttr *RulesetAttr, flags uintptr) (fd int, err error) {
var pointer, size uintptr
// NULL needed for abi version
if rulesetAttr != nil {
pointer = uintptr(unsafe.Pointer(rulesetAttr))
size = unsafe.Sizeof(*rulesetAttr)
}
rulesetFd, _, errno := syscall.Syscall(seccomp.SYS_LANDLOCK_CREATE_RULESET, pointer, size, flags)
fd = int(rulesetFd)
err = errno
if fd < 0 {
return
}
if rulesetAttr != nil { // not a fd otherwise
syscall.CloseOnExec(fd)
}
return fd, nil
}
func LandlockGetABI() (int, error) {
return LandlockCreateRuleset(nil, LANDLOCK_CREATE_RULESET_VERSION)
}
func LandlockRestrictSelf(rulesetFd int, flags uintptr) error {
r, _, errno := syscall.Syscall(seccomp.SYS_LANDLOCK_RESTRICT_SELF, uintptr(rulesetFd), flags, 0)
if r != 0 {
return errno
}
return nil
}

View File

@ -79,6 +79,8 @@ type (
Userns bool `json:"userns,omitempty"` Userns bool `json:"userns,omitempty"`
// share host net namespace // share host net namespace
Net bool `json:"net,omitempty"` Net bool `json:"net,omitempty"`
// share abstract unix socket scope
Abstract bool `json:"abstract,omitempty"`
// allow dangerous terminal I/O // allow dangerous terminal I/O
Tty bool `json:"tty,omitempty"` Tty bool `json:"tty,omitempty"`
// allow multiarch // allow multiarch

View File

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

View File

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

View File

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

View File

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

View File

@ -64,6 +64,10 @@ func (p *Proxy) Start() error {
argF, func(z *container.Container) { argF, func(z *container.Container) {
z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompFlags |= seccomp.AllowMultiarch
z.SeccompPresets |= seccomp.PresetStrict z.SeccompPresets |= seccomp.PresetStrict
// xdg-dbus-proxy fails with scoped abstract unix sockets despite pathname socket being available
z.HostAbstract = true
z.Hostname = "hakurei-dbus" z.Hostname = "hakurei-dbus"
if p.output != nil { if p.output != nil {
z.Stdout, z.Stderr = p.output, p.output z.Stdout, z.Stderr = p.output, p.output

View File

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

View File

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

View File

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

View File

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

View File

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