From 3cb58b4b72ddb9ae1e354b954ae15230c9a49d6d Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sat, 6 Dec 2025 21:16:27 +0900 Subject: [PATCH] internal/pipewire: high level SecurityContext helper This sets up close pipe and socket internally, and exposes the resulting pathname socket and close_fd cleanup as an io.Closer. Signed-off-by: Ophestra --- internal/pipewire/securitycontext.go | 67 ++++++++++++++++++++++++++++ internal/wayland/conn.go | 1 + 2 files changed, 68 insertions(+) diff --git a/internal/pipewire/securitycontext.go b/internal/pipewire/securitycontext.go index 42eb16c..f07d5dc 100644 --- a/internal/pipewire/securitycontext.go +++ b/internal/pipewire/securitycontext.go @@ -1,5 +1,12 @@ package pipewire +import ( + "errors" + "io" + "os" + "syscall" +) + /* pipewire/extensions/security-context.h */ const ( @@ -104,6 +111,66 @@ func (securityContext *SecurityContext) Create(listenFd, closeFd int, props SPAD ) } +// securityContextCloser holds onto resources associated to the security context. +type securityContextCloser struct { + // Pipe with its write end passed to [SecurityContextCreate.CloseFd]. + closeFds [2]int + // Pathname the socket was bound to. + pathname string +} + +// Close closes both ends of the pipe. +func (scc *securityContextCloser) Close() error { + return errors.Join( + syscall.Close(scc.closeFds[1]), + syscall.Close(scc.closeFds[0]), + // there is still technically a TOCTOU here but this is internal + // and has access to the privileged pipewire socket, so it only + // receives trusted input (e.g. from cmd/hakurei) anyway + os.Remove(scc.pathname), + ) +} + +// BindAndCreate binds a new socket to the specified pathname and pass it to Create. +// It returns an [io.Closer] corresponding to [SecurityContextCreate.CloseFd]. +func (securityContext *SecurityContext) BindAndCreate(pathname string, props SPADict) (io.Closer, error) { + var scc securityContextCloser + + // ensure pathname is available + if f, err := os.Create(pathname); err != nil { + return nil, err + } else if err = f.Close(); err != nil { + _ = os.Remove(pathname) + return nil, err + } else if err = os.Remove(pathname); err != nil { + return nil, err + } + scc.pathname = pathname + + var listenFd int + if fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0); err != nil { + return nil, os.NewSyscallError("socket", err) + } else { + securityContext.ctx.cleanup(func() error { return syscall.Close(fd) }) + listenFd = fd + } + if err := syscall.Bind(listenFd, &syscall.SockaddrUnix{Name: pathname}); err != nil { + return nil, os.NewSyscallError("bind", err) + } else if err = syscall.Listen(listenFd, 0); err != nil { + return nil, os.NewSyscallError("listen", err) + } + + if err := syscall.Pipe2(scc.closeFds[0:], syscall.O_CLOEXEC); err != nil { + _ = os.Remove(pathname) + return nil, err + } + if err := securityContext.Create(listenFd, scc.closeFds[1], props); err != nil { + _ = scc.Close() + return nil, err + } + return &scc, nil +} + func (securityContext *SecurityContext) consume(opcode byte, files []int, _ func(v any)) error { closeReceivedFiles(files...) switch opcode { diff --git a/internal/wayland/conn.go b/internal/wayland/conn.go index 40a909e..7aebafb 100644 --- a/internal/wayland/conn.go +++ b/internal/wayland/conn.go @@ -52,6 +52,7 @@ func New(displayPath, bindPath *check.Absolute, appID, instanceID string) (*Secu if f, err := os.Create(bindPath.String()); err != nil { return nil, &Error{RCreate, bindPath.String(), displayPath.String(), err} } else if err = f.Close(); err != nil { + _ = os.Remove(bindPath.String()) return nil, &Error{RCreate, bindPath.String(), displayPath.String(), err} } else if err = os.Remove(bindPath.String()); err != nil { return nil, &Error{RCreate, bindPath.String(), displayPath.String(), err}