Compare commits
2 Commits
9eb6bc71c4
...
a5baad9e00
Author | SHA1 | Date | |
---|---|---|---|
a5baad9e00 | |||
551ec8c27d |
@ -28,6 +28,8 @@ 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"`
|
||||
|
@ -92,6 +92,8 @@ 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
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
. "syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container/landlock"
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
@ -263,6 +264,12 @@ 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
|
||||
|
14
container/landlock/landlock-helper.c
Normal file
14
container/landlock/landlock-helper.c
Normal file
@ -0,0 +1,14 @@
|
||||
#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;
|
||||
}
|
3
container/landlock/landlock-helper.h
Normal file
3
container/landlock/landlock-helper.h
Normal file
@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
int hakurei_scope_abstract_unix_sockets(int* p_errno, int fd);
|
57
container/landlock/landlock.go
Normal file
57
container/landlock/landlock.go
Normal file
@ -0,0 +1,57 @@
|
||||
package landlock
|
||||
|
||||
/*
|
||||
#cgo linux pkg-config: --static libpsx
|
||||
|
||||
#include <linux/landlock.h>
|
||||
#include <sys/syscall.h>
|
||||
|
||||
#include "landlock-helper.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
|
||||
)
|
||||
|
||||
type LandlockRulesetAttr = C.struct_landlock_ruleset_attr
|
||||
|
||||
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))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -79,6 +79,8 @@ 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
|
||||
|
@ -33,6 +33,7 @@ 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
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
@ -390,6 +391,22 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,6 +137,7 @@ in
|
||||
multiarch
|
||||
env
|
||||
;
|
||||
scope_abstract = app.scopeAbstract;
|
||||
map_real_uid = app.mapRealUid;
|
||||
|
||||
filesystem =
|
||||
|
22
options.md
22
options.md
@ -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:*
|
||||
` true `
|
||||
|
||||
|
@ -182,6 +182,9 @@ 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";
|
||||
|
@ -5,6 +5,7 @@
|
||||
makeBinaryWrapper,
|
||||
xdg-dbus-proxy,
|
||||
pkg-config,
|
||||
libcap,
|
||||
libffi,
|
||||
libseccomp,
|
||||
acl,
|
||||
@ -80,10 +81,16 @@ 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
|
||||
env.GO_TEST_SKIP_ACL = 1;
|
||||
GO_TEST_SKIP_ACL = 1;
|
||||
};
|
||||
|
||||
buildInputs = [
|
||||
libcap
|
||||
libffi
|
||||
libseccomp
|
||||
acl
|
||||
|
@ -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,15 +40,25 @@ 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 {
|
||||
if ec.hasType(a) {
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,10 @@ 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
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
@ -33,6 +34,10 @@ type TestCase struct {
|
||||
FS *FS `json:"fs"`
|
||||
Mount []*MountinfoEntry `json:"mount"`
|
||||
Seccomp bool `json:"seccomp"`
|
||||
|
||||
TrySocket string `json:"try_socket,omitempty"`
|
||||
SocketAbstract bool `json:"socket_abstract,omitempty"`
|
||||
SocketPathname bool `json:"socket_pathname,omitempty"`
|
||||
}
|
||||
|
||||
type T struct {
|
||||
@ -125,6 +130,47 @@ func (t *T) MustCheck(want *TestCase) {
|
||||
} else {
|
||||
printf("[SKIP] skipping seccomp check")
|
||||
}
|
||||
|
||||
if want.TrySocket != "" {
|
||||
abstractConn, abstractErr := net.Dial("unix", "@"+want.TrySocket)
|
||||
pathnameConn, pathnameErr := net.Dial("unix", want.TrySocket)
|
||||
ok := true
|
||||
|
||||
if abstractErr == nil {
|
||||
if err := abstractConn.Close(); err != nil {
|
||||
ok = false
|
||||
log.Printf("Close: %v", err)
|
||||
}
|
||||
}
|
||||
if pathnameErr == nil {
|
||||
if err := pathnameConn.Close(); err != nil {
|
||||
ok = false
|
||||
log.Printf("Close: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
abstractWantErr := error(syscall.EPERM)
|
||||
pathnameWantErr := error(syscall.ENOENT)
|
||||
if want.SocketAbstract {
|
||||
abstractWantErr = nil
|
||||
}
|
||||
if want.SocketPathname {
|
||||
pathnameWantErr = nil
|
||||
}
|
||||
|
||||
if !errors.Is(abstractErr, abstractWantErr) {
|
||||
ok = false
|
||||
log.Printf("abstractErr: %v, want %v", abstractErr, abstractWantErr)
|
||||
}
|
||||
if !errors.Is(pathnameErr, pathnameWantErr) {
|
||||
ok = false
|
||||
log.Printf("pathnameErr: %v, want %v", pathnameErr, pathnameWantErr)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MustCheckFilter(pid int, want string) {
|
||||
|
@ -50,6 +50,9 @@ let
|
||||
useCommonPaths
|
||||
userns
|
||||
;
|
||||
enablements = {
|
||||
inherit (tc) x11;
|
||||
};
|
||||
share = testProgram;
|
||||
packages = [ ];
|
||||
path = "${testProgram}/bin/hakurei-test";
|
||||
|
@ -25,6 +25,7 @@ in
|
||||
mapRealUid = false;
|
||||
useCommonPaths = true;
|
||||
userns = false;
|
||||
x11 = true;
|
||||
|
||||
# 0, PresetStrict
|
||||
expectedFilter = {
|
||||
@ -35,6 +36,7 @@ in
|
||||
want = {
|
||||
env = [
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
|
||||
"DISPLAY=:0"
|
||||
"HOME=/var/lib/hakurei/u0/a4"
|
||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
|
||||
"SHELL=/run/current-system/sw/bin/bash"
|
||||
@ -161,7 +163,9 @@ in
|
||||
} null;
|
||||
devices = fs "800001ed" null null;
|
||||
} null;
|
||||
tmp = fs "800001f8" { } null;
|
||||
tmp = fs "800001f8" {
|
||||
".X11-unix" = fs "801001ff" { X0 = fs "10001fd" null null; } null;
|
||||
} null;
|
||||
usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
|
||||
var = fs "800001c0" {
|
||||
lib = fs "800001c0" {
|
||||
@ -231,10 +235,15 @@ in
|
||||
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
|
||||
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
|
||||
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
|
||||
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
];
|
||||
|
||||
seccomp = true;
|
||||
|
||||
try_socket = "/tmp/.X11-unix/X0";
|
||||
socket_abstract = false;
|
||||
socket_pathname = true;
|
||||
};
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ in
|
||||
mapRealUid = true;
|
||||
useCommonPaths = true;
|
||||
userns = false;
|
||||
x11 = false;
|
||||
|
||||
# 0, PresetStrict
|
||||
expectedFilter = {
|
||||
@ -266,5 +267,9 @@ in
|
||||
];
|
||||
|
||||
seccomp = true;
|
||||
|
||||
try_socket = "/tmp/.X11-unix/X0";
|
||||
socket_abstract = false;
|
||||
socket_pathname = false;
|
||||
};
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ in
|
||||
mapRealUid = false;
|
||||
useCommonPaths = false;
|
||||
userns = true;
|
||||
x11 = false;
|
||||
|
||||
# 0, PresetExt | PresetDenyDevel
|
||||
expectedFilter = {
|
||||
@ -261,5 +262,9 @@ in
|
||||
];
|
||||
|
||||
seccomp = true;
|
||||
|
||||
try_socket = "/tmp/.X11-unix/X0";
|
||||
socket_abstract = false;
|
||||
socket_pathname = false;
|
||||
};
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ in
|
||||
mapRealUid = false;
|
||||
useCommonPaths = false;
|
||||
userns = false;
|
||||
x11 = false;
|
||||
|
||||
# 0, PresetStrict
|
||||
expectedFilter = {
|
||||
@ -259,5 +260,9 @@ in
|
||||
];
|
||||
|
||||
seccomp = true;
|
||||
|
||||
try_socket = "/tmp/.X11-unix/X0";
|
||||
socket_abstract = false;
|
||||
socket_pathname = false;
|
||||
};
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ in
|
||||
mapRealUid = false;
|
||||
useCommonPaths = true;
|
||||
userns = false;
|
||||
x11 = true;
|
||||
|
||||
# 0, PresetExt | PresetDenyNS | PresetDenyDevel
|
||||
expectedFilter = {
|
||||
@ -44,6 +45,7 @@ in
|
||||
want = {
|
||||
env = [
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
|
||||
"DISPLAY=:0"
|
||||
"HOME=/var/lib/hakurei/u0/a2"
|
||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
|
||||
"SHELL=/run/current-system/sw/bin/bash"
|
||||
@ -188,7 +190,9 @@ in
|
||||
} null;
|
||||
devices = fs "800001ed" null null;
|
||||
} null;
|
||||
tmp = fs "800001f8" { } null;
|
||||
tmp = fs "800001f8" {
|
||||
".X11-unix" = fs "801001ff" { X0 = fs "10001fd" null null; } null;
|
||||
} null;
|
||||
usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
|
||||
var = fs "800001c0" {
|
||||
lib = fs "800001c0" {
|
||||
@ -263,10 +267,15 @@ in
|
||||
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
|
||||
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
|
||||
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
|
||||
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
|
||||
];
|
||||
|
||||
seccomp = true;
|
||||
|
||||
try_socket = "/tmp/.X11-unix/X0";
|
||||
socket_abstract = false;
|
||||
socket_pathname = true;
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user