aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorClayton Gilmer <netadr_t@outlook.com>2025-08-18 12:00:52 +0900
committerOphestra <cat@gensokyo.uk>2025-08-18 16:28:14 +0900
commit5db07140726e6e1bf615ed075e9a8b653508c2a7 (patch)
tree15cdb5ed164ec5cdc32527f87ee969ad704cdd11
parent69a4ab81053ef31d745ac0d92531cefb7d3a3e44 (diff)
container: optionally isolate host abstract UNIX domain sockets via landlock
-rw-r--r--cmd/hpkg/app.go3
-rw-r--r--container/container.go47
-rw-r--r--container/landlock.go239
-rw-r--r--container/landlock_test.go61
-rw-r--r--hst/config.go2
-rw-r--r--internal/app/app_pd_linux_test.go2
-rw-r--r--internal/app/container_linux.go1
-rw-r--r--internal/app/seal_linux.go9
-rw-r--r--nixos.nix1
-rw-r--r--options.nix1
-rw-r--r--system/dbus/proc.go4
-rw-r--r--test/sandbox/case/device.nix2
-rw-r--r--test/sandbox/case/mapuid.nix2
-rw-r--r--test/sandbox/case/pd.nix4
-rw-r--r--test/sandbox/case/pdlike.nix2
-rw-r--r--test/sandbox/case/preset.nix2
-rw-r--r--test/sandbox/case/tty.nix2
17 files changed, 375 insertions, 9 deletions
diff --git a/cmd/hpkg/app.go b/cmd/hpkg/app.go
index 67d12496..713a4fc7 100644
--- a/cmd/hpkg/app.go
+++ b/cmd/hpkg/app.go
@@ -28,6 +28,8 @@ type appInfo struct {
// passed through to [hst.Config]
Net bool `json:"net,omitempty"`
// passed through to [hst.Config]
+ Abstract bool `json:"abstract,omitempty"`
+ // passed through to [hst.Config]
Device bool `json:"dev,omitempty"`
// passed through to [hst.Config]
Tty bool `json:"tty,omitempty"`
@@ -87,6 +89,7 @@ func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, arg
Devel: app.Devel,
Userns: app.Userns,
Net: app.Net,
+ Abstract: app.Abstract,
Device: app.Device,
Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID,
diff --git a/container/container.go b/container/container.go
index 52691cea..fa5a34e5 100644
--- a/container/container.go
+++ b/container/container.go
@@ -92,6 +92,8 @@ type (
RetainSession bool
// Do not [syscall.CLONE_NEWNET].
HostNet bool
+ // Do not [LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET].
+ HostAbstract bool
// Retain CAP_SYS_ADMIN.
Privileged bool
}
@@ -185,6 +187,51 @@ func (p *Container) Start() error {
"prctl(PR_SET_NO_NEW_PRIVS):")
}
+ // landlock: depends on per-thread state but acts on a process group
+ {
+ rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL}
+ if !p.HostAbstract {
+ rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
+ }
+
+ 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)
+ }
+
+ if rulesetFd, err := rulesetAttr.Create(0); err != nil {
+ return wrapErrSuffix(err,
+ "cannot create landlock ruleset:")
+ } else {
+ msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
+ 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")
if err := p.cmd.Start(); err != nil {
return msg.WrapErr(err, err.Error())
diff --git a/container/landlock.go b/container/landlock.go
new file mode 100644
index 00000000..7f7461e2
--- /dev/null
+++ b/container/landlock.go
@@ -0,0 +1,239 @@
+package container
+
+import (
+ "strings"
+ "syscall"
+ "unsafe"
+
+ "hakurei.app/container/seccomp"
+)
+
+// include/uapi/linux/landlock.h
+
+const (
+ LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
+)
+
+type LandlockAccessFS uintptr
+
+const (
+ LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 1 << iota
+ LANDLOCK_ACCESS_FS_WRITE_FILE
+ LANDLOCK_ACCESS_FS_READ_FILE
+ LANDLOCK_ACCESS_FS_READ_DIR
+ LANDLOCK_ACCESS_FS_REMOVE_DIR
+ LANDLOCK_ACCESS_FS_REMOVE_FILE
+ LANDLOCK_ACCESS_FS_MAKE_CHAR
+ LANDLOCK_ACCESS_FS_MAKE_DIR
+ LANDLOCK_ACCESS_FS_MAKE_REG
+ LANDLOCK_ACCESS_FS_MAKE_SOCK
+ LANDLOCK_ACCESS_FS_MAKE_FIFO
+ LANDLOCK_ACCESS_FS_MAKE_BLOCK
+ LANDLOCK_ACCESS_FS_MAKE_SYM
+ LANDLOCK_ACCESS_FS_REFER
+ LANDLOCK_ACCESS_FS_TRUNCATE
+ LANDLOCK_ACCESS_FS_IOCTL_DEV
+
+ _LANDLOCK_ACCESS_FS_DELIM
+)
+
+func (f LandlockAccessFS) String() string {
+ switch f {
+ case LANDLOCK_ACCESS_FS_EXECUTE:
+ return "execute"
+
+ case LANDLOCK_ACCESS_FS_WRITE_FILE:
+ return "write_file"
+
+ case LANDLOCK_ACCESS_FS_READ_FILE:
+ return "read_file"
+
+ case LANDLOCK_ACCESS_FS_READ_DIR:
+ return "read_dir"
+
+ case LANDLOCK_ACCESS_FS_REMOVE_DIR:
+ return "remove_dir"
+
+ case LANDLOCK_ACCESS_FS_REMOVE_FILE:
+ return "remove_file"
+
+ case LANDLOCK_ACCESS_FS_MAKE_CHAR:
+ return "make_char"
+
+ case LANDLOCK_ACCESS_FS_MAKE_DIR:
+ return "make_dir"
+
+ case LANDLOCK_ACCESS_FS_MAKE_REG:
+ return "make_reg"
+
+ case LANDLOCK_ACCESS_FS_MAKE_SOCK:
+ return "make_sock"
+
+ case LANDLOCK_ACCESS_FS_MAKE_FIFO:
+ return "make_fifo"
+
+ case LANDLOCK_ACCESS_FS_MAKE_BLOCK:
+ return "make_block"
+
+ case LANDLOCK_ACCESS_FS_MAKE_SYM:
+ return "make_sym"
+
+ case LANDLOCK_ACCESS_FS_REFER:
+ return "fs_refer"
+
+ case LANDLOCK_ACCESS_FS_TRUNCATE:
+ return "fs_truncate"
+
+ case LANDLOCK_ACCESS_FS_IOCTL_DEV:
+ return "fs_ioctl_dev"
+
+ default:
+ var c []LandlockAccessFS
+ for i := LandlockAccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
+ if f&i != 0 {
+ c = append(c, i)
+ }
+ }
+ if len(c) == 0 {
+ return "NULL"
+ }
+ s := make([]string, len(c))
+ for i, v := range c {
+ s[i] = v.String()
+ }
+ return strings.Join(s, " ")
+ }
+}
+
+type LandlockAccessNet uintptr
+
+const (
+ LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota
+ LANDLOCK_ACCESS_NET_CONNECT_TCP
+
+ _LANDLOCK_ACCESS_NET_DELIM
+)
+
+func (f LandlockAccessNet) String() string {
+ switch f {
+ case LANDLOCK_ACCESS_NET_BIND_TCP:
+ return "bind_tcp"
+
+ case LANDLOCK_ACCESS_NET_CONNECT_TCP:
+ return "connect_tcp"
+
+ default:
+ var c []LandlockAccessNet
+ for i := LandlockAccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
+ if f&i != 0 {
+ c = append(c, i)
+ }
+ }
+ if len(c) == 0 {
+ return "NULL"
+ }
+ s := make([]string, len(c))
+ for i, v := range c {
+ s[i] = v.String()
+ }
+ return strings.Join(s, " ")
+ }
+}
+
+type LandlockScope uintptr
+
+const (
+ LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota
+ LANDLOCK_SCOPE_SIGNAL
+
+ _LANDLOCK_SCOPE_DELIM
+)
+
+func (f LandlockScope) String() string {
+ switch f {
+ case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET:
+ return "abstract_unix_socket"
+
+ case LANDLOCK_SCOPE_SIGNAL:
+ return "signal"
+
+ default:
+ var c []LandlockScope
+ for i := LandlockScope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
+ if f&i != 0 {
+ c = append(c, i)
+ }
+ }
+ if len(c) == 0 {
+ return "NULL"
+ }
+ s := make([]string, len(c))
+ for i, v := range c {
+ s[i] = v.String()
+ }
+ return strings.Join(s, " ")
+ }
+}
+
+type RulesetAttr struct {
+ // Bitmask of handled filesystem actions.
+ HandledAccessFS LandlockAccessFS
+ // Bitmask of handled network actions.
+ HandledAccessNet LandlockAccessNet
+ // Bitmask of scopes restricting a Landlock domain from accessing outside resources (e.g. IPCs).
+ Scoped LandlockScope
+}
+
+func (rulesetAttr *RulesetAttr) String() string {
+ if rulesetAttr == nil {
+ return "NULL"
+ }
+ elems := make([]string, 0, 3)
+ if rulesetAttr.HandledAccessFS > 0 {
+ elems = append(elems, "fs: "+rulesetAttr.HandledAccessFS.String())
+ }
+ if rulesetAttr.HandledAccessNet > 0 {
+ elems = append(elems, "net: "+rulesetAttr.HandledAccessNet.String())
+ }
+ if rulesetAttr.Scoped > 0 {
+ elems = append(elems, "scoped: "+rulesetAttr.Scoped.String())
+ }
+ if len(elems) == 0 {
+ return "0"
+ }
+ return strings.Join(elems, ", ")
+}
+
+func (rulesetAttr *RulesetAttr) Create(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 (*RulesetAttr)(nil).Create(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
+}
diff --git a/container/landlock_test.go b/container/landlock_test.go
new file mode 100644
index 00000000..d204bc31
--- /dev/null
+++ b/container/landlock_test.go
@@ -0,0 +1,61 @@
+package container_test
+
+import (
+ "testing"
+ "unsafe"
+
+ "hakurei.app/container"
+)
+
+func TestLandlockString(t *testing.T) {
+ testCases := []struct {
+ name string
+ rulesetAttr *container.RulesetAttr
+ want string
+ }{
+ {"nil", nil, "NULL"},
+ {"zero", new(container.RulesetAttr), "0"},
+ {"some", &container.RulesetAttr{Scoped: container.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
+ {"set", &container.RulesetAttr{
+ HandledAccessFS: container.LANDLOCK_ACCESS_FS_MAKE_SYM | container.LANDLOCK_ACCESS_FS_IOCTL_DEV | container.LANDLOCK_ACCESS_FS_WRITE_FILE,
+ HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP,
+ Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | container.LANDLOCK_SCOPE_SIGNAL,
+ }, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
+ {"all", &container.RulesetAttr{
+ HandledAccessFS: container.LANDLOCK_ACCESS_FS_EXECUTE |
+ container.LANDLOCK_ACCESS_FS_WRITE_FILE |
+ container.LANDLOCK_ACCESS_FS_READ_FILE |
+ container.LANDLOCK_ACCESS_FS_READ_DIR |
+ container.LANDLOCK_ACCESS_FS_REMOVE_DIR |
+ container.LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ container.LANDLOCK_ACCESS_FS_MAKE_CHAR |
+ container.LANDLOCK_ACCESS_FS_MAKE_DIR |
+ container.LANDLOCK_ACCESS_FS_MAKE_REG |
+ container.LANDLOCK_ACCESS_FS_MAKE_SOCK |
+ container.LANDLOCK_ACCESS_FS_MAKE_FIFO |
+ container.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
+ container.LANDLOCK_ACCESS_FS_MAKE_SYM |
+ container.LANDLOCK_ACCESS_FS_REFER |
+ container.LANDLOCK_ACCESS_FS_TRUNCATE |
+ container.LANDLOCK_ACCESS_FS_IOCTL_DEV,
+ HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP |
+ container.LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
+ container.LANDLOCK_SCOPE_SIGNAL,
+ }, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ if got := tc.rulesetAttr.String(); got != tc.want {
+ t.Errorf("String: %s, want %s", got, tc.want)
+ }
+ })
+ }
+}
+
+func TestLandlockAttrSize(t *testing.T) {
+ want := 24
+ if got := unsafe.Sizeof(container.RulesetAttr{}); got != uintptr(want) {
+ t.Errorf("Sizeof: %d, want %d", got, want)
+ }
+}
diff --git a/hst/config.go b/hst/config.go
index 34100e4f..d6f07be0 100644
--- a/hst/config.go
+++ b/hst/config.go
@@ -79,6 +79,8 @@ type (
Userns bool `json:"userns,omitempty"`
// share host net namespace
Net bool `json:"net,omitempty"`
+ // share abstract unix socket scope
+ Abstract bool `json:"abstract,omitempty"`
// allow dangerous terminal I/O
Tty bool `json:"tty,omitempty"`
// allow multiarch
diff --git a/internal/app/app_pd_linux_test.go b/internal/app/app_pd_linux_test.go
index 109e8816..96182d31 100644
--- a/internal/app/app_pd_linux_test.go
+++ b/internal/app/app_pd_linux_test.go
@@ -62,6 +62,7 @@ var testCasesPd = []sealTestCase{
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true,
+ HostAbstract: true,
RetainSession: true,
ForwardCancel: true,
},
@@ -203,6 +204,7 @@ var testCasesPd = []sealTestCase{
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true,
+ HostAbstract: true,
RetainSession: true,
ForwardCancel: true,
},
diff --git a/internal/app/container_linux.go b/internal/app/container_linux.go
index 3f1259fd..7f088192 100644
--- a/internal/app/container_linux.go
+++ b/internal/app/container_linux.go
@@ -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,
+ HostAbstract: s.Abstract,
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
// this behaviour is implemented in the shim
diff --git a/internal/app/seal_linux.go b/internal/app/seal_linux.go
index c7293720..0ae121fb 100644
--- a/internal/app/seal_linux.go
+++ b/internal/app/seal_linux.go
@@ -238,10 +238,11 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
}
conf := &hst.ContainerConfig{
- Userns: true,
- Net: true,
- Tty: true,
- AutoEtc: true,
+ Userns: true,
+ Net: true,
+ Abstract: true,
+ Tty: true,
+ AutoEtc: true,
AutoRoot: container.AbsFHSRoot,
RootFlags: container.BindWritable,
diff --git a/nixos.nix b/nixos.nix
index 4f083bcb..eb8df5f8 100644
--- a/nixos.nix
+++ b/nixos.nix
@@ -132,6 +132,7 @@ in
devel
userns
net
+ abstract
device
tty
multiarch
diff --git a/options.nix b/options.nix
index a51609c9..ec2533c0 100644
--- a/options.nix
+++ b/options.nix
@@ -182,6 +182,7 @@ in
net = mkEnableOption "network access" // {
default = true;
};
+ abstract = mkEnableOption "abstract unix domain socket access";
nix = mkEnableOption "nix daemon access";
mapRealUid = mkEnableOption "mapping to priv-user uid";
diff --git a/system/dbus/proc.go b/system/dbus/proc.go
index 0e5f1cef..6607ef19 100644
--- a/system/dbus/proc.go
+++ b/system/dbus/proc.go
@@ -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 fails with scoped abstract unix sockets despite pathname socket being available
+ z.HostAbstract = true
+
z.Hostname = "hakurei-dbus"
if p.output != nil {
z.Stdout, z.Stderr = p.output, p.output
diff --git a/test/sandbox/case/device.nix b/test/sandbox/case/device.nix
index e9cf724b..c1a2f5f5 100644
--- a/test/sandbox/case/device.nix
+++ b/test/sandbox/case/device.nix
@@ -243,7 +243,7 @@ in
seccomp = true;
try_socket = "/tmp/.X11-unix/X0";
- socket_abstract = true;
+ socket_abstract = false;
socket_pathname = true;
};
}
diff --git a/test/sandbox/case/mapuid.nix b/test/sandbox/case/mapuid.nix
index 18d82e77..3ef5f247 100644
--- a/test/sandbox/case/mapuid.nix
+++ b/test/sandbox/case/mapuid.nix
@@ -269,7 +269,7 @@ in
seccomp = true;
try_socket = "/tmp/.X11-unix/X0";
- socket_abstract = true;
+ socket_abstract = false;
socket_pathname = false;
};
}
diff --git a/test/sandbox/case/pd.nix b/test/sandbox/case/pd.nix
index 79400f53..49230bcf 100644
--- a/test/sandbox/case/pd.nix
+++ b/test/sandbox/case/pd.nix
@@ -194,5 +194,9 @@
];
seccomp = true;
+
+ try_socket = "/tmp/.X11-unix/X0";
+ socket_abstract = true;
+ socket_pathname = false;
};
}
diff --git a/test/sandbox/case/pdlike.nix b/test/sandbox/case/pdlike.nix
index da6753f8..ea341478 100644
--- a/test/sandbox/case/pdlike.nix
+++ b/test/sandbox/case/pdlike.nix
@@ -264,7 +264,7 @@ in
seccomp = true;
try_socket = "/tmp/.X11-unix/X0";
- socket_abstract = true;
+ socket_abstract = false;
socket_pathname = false;
};
}
diff --git a/test/sandbox/case/preset.nix b/test/sandbox/case/preset.nix
index be376a17..41f612f8 100644
--- a/test/sandbox/case/preset.nix
+++ b/test/sandbox/case/preset.nix
@@ -262,7 +262,7 @@ in
seccomp = true;
try_socket = "/tmp/.X11-unix/X0";
- socket_abstract = true;
+ socket_abstract = false;
socket_pathname = false;
};
}
diff --git a/test/sandbox/case/tty.nix b/test/sandbox/case/tty.nix
index ff7bf035..d95cbb83 100644
--- a/test/sandbox/case/tty.nix
+++ b/test/sandbox/case/tty.nix
@@ -275,7 +275,7 @@ in
seccomp = true;
try_socket = "/tmp/.X11-unix/X0";
- socket_abstract = true;
+ socket_abstract = false;
socket_pathname = true;
};
}