From 83a1c75f1ac4d2d611b3a96474ed07df3cb557b6 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Mon, 18 Aug 2025 02:24:56 +0900 Subject: [PATCH] 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 https://git.gensokyo.uk/security/hakurei/pulls/1. Signed-off-by: Ophestra --- internal/app/seal_linux.go | 28 ++++++++++++++++++++- test/sandbox/assert.go | 46 +++++++++++++++++++++++++++++++++++ test/sandbox/case/default.nix | 3 +++ test/sandbox/case/device.nix | 11 ++++++++- test/sandbox/case/mapuid.nix | 5 ++++ test/sandbox/case/pdlike.nix | 5 ++++ test/sandbox/case/preset.nix | 5 ++++ test/sandbox/case/tty.nix | 11 ++++++++- 8 files changed, 111 insertions(+), 3 deletions(-) diff --git a/internal/app/seal_linux.go b/internal/app/seal_linux.go index 49ef78f..c729372 100644 --- a/internal/app/seal_linux.go +++ b/internal/app/seal_linux.go @@ -12,6 +12,7 @@ import ( "path" "regexp" "slices" + "strconv" "strings" "sync/atomic" "syscall" @@ -386,9 +387,34 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co return hlog.WrapErr(ErrXDisplay, "DISPLAY is not set") } else { + socketDir := container.AbsFHSTmp.Append(".X11-unix") + + // 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 { + 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 - socketDir := container.AbsFHSTmp.Append(".X11-unix") seal.container.Bind(socketDir, socketDir, 0) } } diff --git a/test/sandbox/assert.go b/test/sandbox/assert.go index 57d1703..2c2c9f2 100644 --- a/test/sandbox/assert.go +++ b/test/sandbox/assert.go @@ -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) { diff --git a/test/sandbox/case/default.nix b/test/sandbox/case/default.nix index 6db1ff4..ec814aa 100644 --- a/test/sandbox/case/default.nix +++ b/test/sandbox/case/default.nix @@ -50,6 +50,9 @@ let useCommonPaths userns ; + enablements = { + inherit (tc) x11; + }; share = testProgram; packages = [ ]; path = "${testProgram}/bin/hakurei-test"; diff --git a/test/sandbox/case/device.nix b/test/sandbox/case/device.nix index fb2a54f..e9cf724 100644 --- a/test/sandbox/case/device.nix +++ b/test/sandbox/case/device.nix @@ -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=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" @@ -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 = true; + socket_pathname = true; }; } diff --git a/test/sandbox/case/mapuid.nix b/test/sandbox/case/mapuid.nix index 1069f9f..18d82e7 100644 --- a/test/sandbox/case/mapuid.nix +++ b/test/sandbox/case/mapuid.nix @@ -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 = true; + socket_pathname = false; }; } diff --git a/test/sandbox/case/pdlike.nix b/test/sandbox/case/pdlike.nix index 4f0e71c..da6753f 100644 --- a/test/sandbox/case/pdlike.nix +++ b/test/sandbox/case/pdlike.nix @@ -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 = true; + socket_pathname = false; }; } diff --git a/test/sandbox/case/preset.nix b/test/sandbox/case/preset.nix index 60257ad..be376a1 100644 --- a/test/sandbox/case/preset.nix +++ b/test/sandbox/case/preset.nix @@ -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 = true; + socket_pathname = false; }; } diff --git a/test/sandbox/case/tty.nix b/test/sandbox/case/tty.nix index 267ac66..ff7bf03 100644 --- a/test/sandbox/case/tty.nix +++ b/test/sandbox/case/tty.nix @@ -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=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" @@ -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 = true; + socket_pathname = true; }; }