1 Commits

Author SHA1 Message Date
df389e239f internal/pipewire: integrate pw_security_context
All checks were successful
Test / Create distribution (push) Successful in 41s
Test / Create distribution (pull_request) Successful in 36s
Test / Sandbox (pull_request) Successful in 2m28s
Test / Sandbox (push) Successful in 2m36s
Test / Hakurei (push) Successful in 3m21s
Test / Hakurei (pull_request) Successful in 3m15s
Test / Hpkg (pull_request) Successful in 4m6s
Test / Hpkg (push) Successful in 4m16s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Sandbox (race detector) (pull_request) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Hakurei (race detector) (pull_request) Successful in 5m10s
Test / Flake checks (push) Successful in 1m36s
Test / Flake checks (pull_request) Successful in 1m37s
This is required for securely providing access to PipeWire.

This change has already been manually tested and confirmed to work correctly.

This unfortunately cannot be upstreamed in its current state as libpipewire-0.3 breaks static linking.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-19 06:56:32 +09:00
5 changed files with 78 additions and 11 deletions

View File

@@ -12,18 +12,32 @@ import (
type SecurityContext struct { type SecurityContext struct {
// Pipe with its write end passed to the PipeWire security context. // Pipe with its write end passed to the PipeWire security context.
closeFds [2]int closeFds [2]int
// Absolute pathname the socket was bound to.
bindPath *check.Absolute
} }
// Close releases any resources held by [SecurityContext], and prevents further // Close releases any resources held by [SecurityContext], and prevents further
// connections to its associated socket. // connections to its associated socket.
//
// A non-nil error has the concrete type [Error].
func (sc *SecurityContext) Close() error { func (sc *SecurityContext) Close() error {
if sc == nil { if sc == nil || sc.bindPath == nil {
return os.ErrInvalid return os.ErrInvalid
} }
return errors.Join(
e := Error{RCleanup, sc.bindPath.String(), errors.Join(
syscall.Close(sc.closeFds[1]), syscall.Close(sc.closeFds[1]),
syscall.Close(sc.closeFds[0]), syscall.Close(sc.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(sc.bindPath.String()),
)}
if e.Errno != nil {
return &e
}
return nil
} }
// New creates a new security context on the PipeWire remote at remotePath // New creates a new security context on the PipeWire remote at remotePath
@@ -61,11 +75,11 @@ func New(remotePath, bindPath *check.Absolute) (*SecurityContext, error) {
remotePathVal, remotePathVal,
closeFds[1], closeFds[1],
); err != nil { ); err != nil {
return nil, errors.Join(err, return nil, errors.Join(err, // already wrapped
syscall.Close(closeFds[1]), syscall.Close(closeFds[1]),
syscall.Close(closeFds[0]), syscall.Close(closeFds[0]),
) )
} else { } else {
return &SecurityContext{closeFds}, nil return &SecurityContext{closeFds, bindPath}, nil
} }
} }

View File

@@ -3,6 +3,7 @@ package pipewire
import ( import (
"errors" "errors"
"os" "os"
"path"
"reflect" "reflect"
"syscall" "syscall"
"testing" "testing"
@@ -11,7 +12,7 @@ import (
) )
func TestSecurityContextClose(t *testing.T) { func TestSecurityContextClose(t *testing.T) {
t.Parallel() // do not parallel: fd test not thread safe
if err := (*SecurityContext)(nil).Close(); !reflect.DeepEqual(err, os.ErrInvalid) { if err := (*SecurityContext)(nil).Close(); !reflect.DeepEqual(err, os.ErrInvalid) {
t.Fatalf("Close: error = %v", err) t.Fatalf("Close: error = %v", err)
@@ -21,13 +22,24 @@ func TestSecurityContextClose(t *testing.T) {
if err := syscall.Pipe2(ctx.closeFds[0:], syscall.O_CLOEXEC); err != nil { if err := syscall.Pipe2(ctx.closeFds[0:], syscall.O_CLOEXEC); err != nil {
t.Fatalf("Pipe: error = %v", err) t.Fatalf("Pipe: error = %v", err)
} }
if f, err := os.Create(path.Join(t.TempDir(), "remove")); err != nil {
t.Fatal(err)
} else {
ctx.bindPath = check.MustAbs(f.Name())
}
t.Cleanup(func() { _ = syscall.Close(ctx.closeFds[0]); _ = syscall.Close(ctx.closeFds[1]) }) t.Cleanup(func() { _ = syscall.Close(ctx.closeFds[0]); _ = syscall.Close(ctx.closeFds[1]) })
if err := ctx.Close(); err != nil { if err := ctx.Close(); err != nil {
t.Fatalf("Close: error = %v", err) t.Fatalf("Close: error = %v", err)
} else if _, err = os.Stat(ctx.bindPath.String()); err == nil || !errors.Is(err, os.ErrNotExist) {
t.Fatalf("Did not remove %q", ctx.bindPath)
} }
wantErr := errors.Join(syscall.EBADF, syscall.EBADF) wantErr := &Error{Cause: RCleanup, Path: ctx.bindPath.String(), Errno: errors.Join(syscall.EBADF, syscall.EBADF, &os.PathError{
Op: "remove",
Path: ctx.bindPath.String(),
Err: syscall.ENOENT,
})}
if err := ctx.Close(); !reflect.DeepEqual(err, wantErr) { if err := ctx.Close(); !reflect.DeepEqual(err, wantErr) {
t.Fatalf("Close: error = %#v, want %#v", err, wantErr) t.Fatalf("Close: error = %#v, want %#v", err, wantErr)
} }

View File

@@ -24,6 +24,8 @@ typedef enum {
/* ensure pathname failed, implemented in conn.go */ /* ensure pathname failed, implemented in conn.go */
HAKUREI_PIPEWIRE_CREAT, HAKUREI_PIPEWIRE_CREAT,
/* cleanup failed, implemented in conn.go */
HAKUREI_PIPEWIRE_CLEANUP,
} hakurei_pipewire_res; } hakurei_pipewire_res;
hakurei_pipewire_res hakurei_pw_security_context_bind( hakurei_pipewire_res hakurei_pw_security_context_bind(

View File

@@ -10,7 +10,9 @@ package pipewire
import "C" import "C"
import ( import (
"errors" "errors"
"os"
"strings" "strings"
"syscall"
) )
const ( const (
@@ -69,6 +71,9 @@ const (
// RCreate is returned if ensuring pathname availability failed. Returned by [New]. // RCreate is returned if ensuring pathname availability failed. Returned by [New].
RCreate Res = C.HAKUREI_PIPEWIRE_CREAT RCreate Res = C.HAKUREI_PIPEWIRE_CREAT
// RCleanup is returned if cleanup fails. Returned by [SecurityContext.Close].
RCleanup Res = C.HAKUREI_PIPEWIRE_CLEANUP
) )
func (e *Error) Unwrap() error { return e.Errno } func (e *Error) Unwrap() error { return e.Errno }
@@ -111,6 +116,19 @@ func (e *Error) Error() string {
} }
return e.Errno.Error() return e.Errno.Error()
case RCleanup:
var pathError *os.PathError
if errors.As(e.Errno, &pathError) && pathError != nil {
return pathError.Error()
}
var errno syscall.Errno
if errors.As(e.Errno, &errno) && errno != 0 {
return "cannot close pipewire close_fd pipe: " + errno.Error()
}
return e.withPrefix("cannot hang up pipewire security context")
default: default:
return e.withPrefix("impossible outcome") /* not reached */ return e.withPrefix("impossible outcome") /* not reached */
} }

View File

@@ -63,15 +63,15 @@ func TestError(t *testing.T) {
{"bind", Error{ {"bind", Error{
Cause: RBind, Cause: RBind,
Path: "/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire", Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire",
Errno: stub.UniqueError(6), Errno: stub.UniqueError(6),
}, "cannot bind /hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire: unique error 6 injected by the test suite"}, }, "cannot bind /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire: unique error 6 injected by the test suite"},
{"listen", Error{ {"listen", Error{
Cause: RListen, Cause: RListen,
Path: "/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire", Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire",
Errno: stub.UniqueError(7), Errno: stub.UniqueError(7),
}, "cannot listen on /hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire: unique error 7 injected by the test suite"}, }, "cannot listen on /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire: unique error 7 injected by the test suite"},
{"socket invalid", Error{ {"socket invalid", Error{
Cause: RSocket, Cause: RSocket,
@@ -91,6 +91,27 @@ func TestError(t *testing.T) {
Errno: &os.PathError{Op: "create", Path: "/proc/nonexistent", Err: syscall.EEXIST}, Errno: &os.PathError{Op: "create", Path: "/proc/nonexistent", Err: syscall.EEXIST},
}, "create /proc/nonexistent: file exists"}, }, "create /proc/nonexistent: file exists"},
{"cleanup", Error{
Cause: RCleanup,
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire",
}, "cannot hang up pipewire security context"},
{"cleanup PathError", Error{
Cause: RCleanup,
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire",
Errno: errors.Join(syscall.EINVAL, &os.PathError{
Op: "remove",
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire",
Err: stub.UniqueError(9),
}),
}, "remove /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire: unique error 9 injected by the test suite"},
{"cleanup errno", Error{
Cause: RCleanup,
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire",
Errno: errors.Join(syscall.EINVAL),
}, "cannot close pipewire close_fd pipe: invalid argument"},
{"invalid", Error{ {"invalid", Error{
Cause: 0xbad, Cause: 0xbad,
}, "impossible outcome"}, }, "impossible outcome"},