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
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>
This commit is contained in:
parent
5af08cb9bf
commit
df389e239f
@ -28,12 +28,12 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Printf("Version:\t%s (libwayland %s)\n", hi.Version, hi.WaylandVersion)
|
|
||||||
t.Printf("User:\t%d\n", hi.User)
|
t.Printf("User:\t%d\n", hi.User)
|
||||||
t.Printf("TempDir:\t%s\n", hi.TempDir)
|
t.Printf("TempDir:\t%s\n", hi.TempDir)
|
||||||
t.Printf("SharePath:\t%s\n", hi.SharePath)
|
t.Printf("SharePath:\t%s\n", hi.SharePath)
|
||||||
t.Printf("RuntimePath:\t%s\n", hi.RuntimePath)
|
t.Printf("RuntimePath:\t%s\n", hi.RuntimePath)
|
||||||
t.Printf("RunDirPath:\t%s\n", hi.RunDirPath)
|
t.Printf("RunDirPath:\t%s\n", hi.RunDirPath)
|
||||||
|
t.Printf("Version:\t%s (libwayland %s) (pipewire %s)\n", hi.Version, hi.WaylandVersion, hi.PipeWireVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// printShowInstance writes a representation of [hst.State] or [hst.Config] to output.
|
// printShowInstance writes a representation of [hst.State] or [hst.Config] to output.
|
||||||
|
|||||||
@ -56,6 +56,8 @@ type Paths struct {
|
|||||||
type Info struct {
|
type Info struct {
|
||||||
// WaylandVersion is the libwayland value of WAYLAND_VERSION.
|
// WaylandVersion is the libwayland value of WAYLAND_VERSION.
|
||||||
WaylandVersion string `json:"WAYLAND_VERSION"`
|
WaylandVersion string `json:"WAYLAND_VERSION"`
|
||||||
|
// PipeWireVersion is the pipewire value of pw_get_headers_version().
|
||||||
|
PipeWireVersion string `json:"pw_get_headers_version"`
|
||||||
|
|
||||||
// Version is a hardcoded version string.
|
// Version is a hardcoded version string.
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"hakurei.app/internal/acl"
|
"hakurei.app/internal/acl"
|
||||||
"hakurei.app/internal/env"
|
"hakurei.app/internal/env"
|
||||||
"hakurei.app/internal/info"
|
"hakurei.app/internal/info"
|
||||||
|
"hakurei.app/internal/pipewire"
|
||||||
"hakurei.app/internal/system"
|
"hakurei.app/internal/system"
|
||||||
"hakurei.app/internal/wayland"
|
"hakurei.app/internal/wayland"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
@ -21,7 +22,7 @@ import (
|
|||||||
//
|
//
|
||||||
// This must not be called from within package outcome.
|
// This must not be called from within package outcome.
|
||||||
func Info() *hst.Info {
|
func Info() *hst.Info {
|
||||||
hi := hst.Info{WaylandVersion: wayland.Version,
|
hi := hst.Info{WaylandVersion: wayland.Version, PipeWireVersion: pipewire.Version,
|
||||||
Version: info.Version(), User: new(Hsu).MustID(nil)}
|
Version: info.Version(), User: new(Hsu).MustID(nil)}
|
||||||
env.CopyPaths().Copy(&hi.Paths, hi.User)
|
env.CopyPaths().Copy(&hi.Paths, hi.User)
|
||||||
return &hi
|
return &hi
|
||||||
|
|||||||
85
internal/pipewire/conn.go
Normal file
85
internal/pipewire/conn.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package pipewire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"hakurei.app/container/check"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecurityContext holds resources associated with a PipeWire security context.
|
||||||
|
type SecurityContext struct {
|
||||||
|
// Pipe with its write end passed to the PipeWire security context.
|
||||||
|
closeFds [2]int
|
||||||
|
// Absolute pathname the socket was bound to.
|
||||||
|
bindPath *check.Absolute
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases any resources held by [SecurityContext], and prevents further
|
||||||
|
// connections to its associated socket.
|
||||||
|
//
|
||||||
|
// A non-nil error has the concrete type [Error].
|
||||||
|
func (sc *SecurityContext) Close() error {
|
||||||
|
if sc == nil || sc.bindPath == nil {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
e := Error{RCleanup, sc.bindPath.String(), errors.Join(
|
||||||
|
syscall.Close(sc.closeFds[1]),
|
||||||
|
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
|
||||||
|
// or auto-detected, and associates it with a new socket bound to bindPath.
|
||||||
|
//
|
||||||
|
// New does not attach a finalizer to the resulting [SecurityContext] struct.
|
||||||
|
// The caller is responsible for calling [SecurityContext.Close].
|
||||||
|
//
|
||||||
|
// A non-nil error unwraps to concrete type [Error].
|
||||||
|
func New(remotePath, bindPath *check.Absolute) (*SecurityContext, error) {
|
||||||
|
// ensure bindPath is available
|
||||||
|
if f, err := os.Create(bindPath.String()); err != nil {
|
||||||
|
return nil, &Error{RCreate, bindPath.String(), err}
|
||||||
|
} else if err = f.Close(); err != nil {
|
||||||
|
return nil, &Error{RCreate, bindPath.String(), err}
|
||||||
|
} else if err = os.Remove(bindPath.String()); err != nil {
|
||||||
|
return nil, &Error{RCreate, bindPath.String(), err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write end passed to PipeWire security context close_fd
|
||||||
|
var closeFds [2]int
|
||||||
|
if err := syscall.Pipe2(closeFds[0:], syscall.O_CLOEXEC); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// zero value causes auto-detect
|
||||||
|
var remotePathVal string
|
||||||
|
if remotePath != nil {
|
||||||
|
remotePathVal = remotePath.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// returned error is already wrapped
|
||||||
|
if err := securityContextBind(
|
||||||
|
bindPath.String(),
|
||||||
|
remotePathVal,
|
||||||
|
closeFds[1],
|
||||||
|
); err != nil {
|
||||||
|
return nil, errors.Join(err, // already wrapped
|
||||||
|
syscall.Close(closeFds[1]),
|
||||||
|
syscall.Close(closeFds[0]),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return &SecurityContext{closeFds, bindPath}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
66
internal/pipewire/conn_test.go
Normal file
66
internal/pipewire/conn_test.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package pipewire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"reflect"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/check"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSecurityContextClose(t *testing.T) {
|
||||||
|
// do not parallel: fd test not thread safe
|
||||||
|
|
||||||
|
if err := (*SecurityContext)(nil).Close(); !reflect.DeepEqual(err, os.ErrInvalid) {
|
||||||
|
t.Fatalf("Close: error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctx SecurityContext
|
||||||
|
if err := syscall.Pipe2(ctx.closeFds[0:], syscall.O_CLOEXEC); err != nil {
|
||||||
|
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]) })
|
||||||
|
|
||||||
|
if err := ctx.Close(); err != nil {
|
||||||
|
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 := &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) {
|
||||||
|
t.Fatalf("Close: error = %#v, want %#v", err, wantErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewEnsure(t *testing.T) {
|
||||||
|
existingDirPath := check.MustAbs(t.TempDir()).Append("dir")
|
||||||
|
if err := os.MkdirAll(existingDirPath.String(), 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
nonexistent := check.MustAbs("/proc/nonexistent")
|
||||||
|
|
||||||
|
wantErr := &Error{RCreate, existingDirPath.String(), &os.PathError{
|
||||||
|
Op: "open",
|
||||||
|
Path: existingDirPath.String(),
|
||||||
|
Err: syscall.EISDIR,
|
||||||
|
}}
|
||||||
|
if _, err := New(
|
||||||
|
nonexistent,
|
||||||
|
existingDirPath,
|
||||||
|
); !reflect.DeepEqual(err, wantErr) {
|
||||||
|
t.Fatalf("New: error = %#v, want %#v", err, wantErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
252
internal/pipewire/pipewire-helper.c
Normal file
252
internal/pipewire/pipewire-helper.c
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
#include "pipewire-helper.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
|
||||||
|
#include <spa/utils/result.h>
|
||||||
|
#include <spa/utils/string.h>
|
||||||
|
#include <spa/utils/ansi.h>
|
||||||
|
#include <spa/debug/pod.h>
|
||||||
|
#include <spa/debug/format.h>
|
||||||
|
#include <spa/debug/types.h>
|
||||||
|
#include <spa/debug/file.h>
|
||||||
|
|
||||||
|
#include <pipewire/pipewire.h>
|
||||||
|
#include <pipewire/extensions/security-context.h>
|
||||||
|
|
||||||
|
/* contains most of the state used by hakurei_pw_security_context_bind,
|
||||||
|
* not ideal, but it is too painful to separate state with the abysmal
|
||||||
|
* API of pipewire */
|
||||||
|
struct hakurei_pw_security_context_state {
|
||||||
|
struct pw_main_loop *loop;
|
||||||
|
struct pw_context *context;
|
||||||
|
|
||||||
|
struct pw_core *core;
|
||||||
|
struct spa_hook core_listener;
|
||||||
|
|
||||||
|
struct pw_registry *registry;
|
||||||
|
struct spa_hook registry_listener;
|
||||||
|
|
||||||
|
struct pw_properties *props;
|
||||||
|
|
||||||
|
struct pw_security_context *sec;
|
||||||
|
|
||||||
|
int pending_create;
|
||||||
|
int create_result;
|
||||||
|
int pending;
|
||||||
|
int done;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* for field global of registry_events */
|
||||||
|
static void registry_event_global(
|
||||||
|
void *data, uint32_t id,
|
||||||
|
uint32_t permissions, const char *type, uint32_t version,
|
||||||
|
const struct spa_dict *props) {
|
||||||
|
struct hakurei_pw_security_context_state *state = data;
|
||||||
|
|
||||||
|
if (spa_streq(type, PW_TYPE_INTERFACE_SecurityContext))
|
||||||
|
state->sec = pw_registry_bind(state->registry, id, type, version, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* for field global_remove of registry_events */
|
||||||
|
static void registry_event_global_remove(void *data, uint32_t id) {} /* no-op */
|
||||||
|
|
||||||
|
static const struct pw_registry_events registry_events = {
|
||||||
|
PW_VERSION_REGISTRY_EVENTS,
|
||||||
|
.global = registry_event_global,
|
||||||
|
.global_remove = registry_event_global_remove,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* for field error of core_events */
|
||||||
|
static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message) {
|
||||||
|
struct hakurei_pw_security_context_state *state = data;
|
||||||
|
|
||||||
|
pw_log_error("error id:%u seq:%d res:%d (%s): %s",
|
||||||
|
id, seq, res, spa_strerror(res), message);
|
||||||
|
|
||||||
|
if (seq == SPA_RESULT_ASYNC_SEQ(state->pending_create))
|
||||||
|
state->create_result = res;
|
||||||
|
|
||||||
|
if (id == PW_ID_CORE && res == -EPIPE) {
|
||||||
|
state->done = true;
|
||||||
|
pw_main_loop_quit(state->loop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct pw_core_events core_events = {
|
||||||
|
PW_VERSION_CORE_EVENTS,
|
||||||
|
.error = on_core_error,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* for field done of stack allocated core_events in roundtrip */
|
||||||
|
static void core_event_done(void *data, uint32_t id, int seq) {
|
||||||
|
struct hakurei_pw_security_context_state *state = data;
|
||||||
|
if (id == PW_ID_CORE && seq == state->pending) {
|
||||||
|
state->done = true;
|
||||||
|
pw_main_loop_quit(state->loop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void roundtrip(struct hakurei_pw_security_context_state *state) {
|
||||||
|
struct spa_hook core_listener;
|
||||||
|
static const struct pw_core_events core_events = {
|
||||||
|
PW_VERSION_CORE_EVENTS,
|
||||||
|
.done = core_event_done,
|
||||||
|
};
|
||||||
|
spa_zero(core_listener);
|
||||||
|
pw_core_add_listener(state->core, &core_listener, &core_events, state);
|
||||||
|
|
||||||
|
state->done = false;
|
||||||
|
state->pending = pw_core_sync(state->core, PW_ID_CORE, 0);
|
||||||
|
|
||||||
|
while (!state->done)
|
||||||
|
pw_main_loop_run(state->loop);
|
||||||
|
|
||||||
|
spa_hook_remove(&core_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
hakurei_pipewire_res hakurei_pw_security_context_bind(
|
||||||
|
char *socket_path,
|
||||||
|
char *remote_path,
|
||||||
|
int close_fd) {
|
||||||
|
hakurei_pipewire_res res = HAKUREI_PIPEWIRE_SUCCESS; /* see pipewire.go for handling */
|
||||||
|
|
||||||
|
struct hakurei_pw_security_context_state state = {0};
|
||||||
|
struct pw_loop *l;
|
||||||
|
struct spa_error_location loc;
|
||||||
|
int listen_fd;
|
||||||
|
struct sockaddr_un sockaddr = {0};
|
||||||
|
|
||||||
|
/* stack allocated because pw_deinit is always called before returning,
|
||||||
|
* in the implementation it actually does nothing with these addresses
|
||||||
|
* and I have no idea why it would even need these, still it is safe to
|
||||||
|
* do this to not risk a future version of pipewire clobbering strings */
|
||||||
|
int fake_argc = 1;
|
||||||
|
char *fake_argv[] = {"hakurei", NULL};
|
||||||
|
/* this makes multiple getenv calls, caller must ensure to NOT setenv
|
||||||
|
* before this function returns */
|
||||||
|
pw_init(&fake_argc, (char ***)&fake_argv);
|
||||||
|
|
||||||
|
/* as far as I can tell, setting engine to "org.flatpak" gets special
|
||||||
|
* treatment, and should never be used here because the .flatpak-info
|
||||||
|
* hack is vulnerable to a confused deputy attack */
|
||||||
|
state.props = pw_properties_new(
|
||||||
|
PW_KEY_SEC_ENGINE, "app.hakurei",
|
||||||
|
PW_KEY_ACCESS, "restricted",
|
||||||
|
NULL);
|
||||||
|
|
||||||
|
/* this is unfortunately required to do ANYTHING with pipewire */
|
||||||
|
state.loop = pw_main_loop_new(NULL);
|
||||||
|
if (state.loop == NULL) {
|
||||||
|
res = HAKUREI_PIPEWIRE_MAINLOOP;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
l = pw_main_loop_get_loop(state.loop);
|
||||||
|
|
||||||
|
/* boilerplate from src/tools/pw-container.c */
|
||||||
|
state.context = pw_context_new(l, NULL, 0);
|
||||||
|
if (state.context == NULL) {
|
||||||
|
res = HAKUREI_PIPEWIRE_CTX;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* boilerplate from src/tools/pw-container.c;
|
||||||
|
* this does not unsetenv, so special handling is not required
|
||||||
|
* unlike for libwayland-client */
|
||||||
|
state.core = pw_context_connect(
|
||||||
|
state.context,
|
||||||
|
pw_properties_new(
|
||||||
|
PW_KEY_REMOTE_INTENTION, "manager",
|
||||||
|
PW_KEY_REMOTE_NAME, remote_path,
|
||||||
|
NULL),
|
||||||
|
0);
|
||||||
|
if (state.core == NULL) {
|
||||||
|
res = HAKUREI_PIPEWIRE_CONNECT;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* obtains the security context */
|
||||||
|
pw_core_add_listener(state.core, &state.core_listener, &core_events, &state);
|
||||||
|
state.registry = pw_core_get_registry(state.core, PW_VERSION_REGISTRY, 0);
|
||||||
|
if (state.registry == NULL) {
|
||||||
|
res = HAKUREI_PIPEWIRE_REGISTRY;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
/* undocumented, this ends up calling registry_method_marshal_add_listener,
|
||||||
|
* which is hard-coded to return 0, note that the function pointer this calls
|
||||||
|
* is uninitialised for some pw_registry objects so if you are using this code
|
||||||
|
* as an example you must keep that in mind */
|
||||||
|
pw_registry_add_listener(state.registry, &state.registry_listener, ®istry_events, &state);
|
||||||
|
roundtrip(&state);
|
||||||
|
if (state.sec == NULL) {
|
||||||
|
res = HAKUREI_PIPEWIRE_NOT_AVAIL;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* socket to attach security context */
|
||||||
|
listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||||
|
if (listen_fd < 0) {
|
||||||
|
res = HAKUREI_PIPEWIRE_SOCKET;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* similar to libwayland, pipewire requires bind and listen to be called
|
||||||
|
* on the socket before being passed to pw_security_context_create */
|
||||||
|
sockaddr.sun_family = AF_UNIX;
|
||||||
|
snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", socket_path);
|
||||||
|
if (bind(listen_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != 0) {
|
||||||
|
res = HAKUREI_PIPEWIRE_BIND;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
if (listen(listen_fd, 0) != 0) {
|
||||||
|
res = HAKUREI_PIPEWIRE_LISTEN;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* attach security context to socket */
|
||||||
|
state.create_result = 0;
|
||||||
|
state.pending_create = pw_security_context_create(state.sec, listen_fd, close_fd, &state.props->dict);
|
||||||
|
if (SPA_RESULT_IS_ASYNC(state.pending_create)) {
|
||||||
|
pw_log_debug("create: %d", state.pending_create);
|
||||||
|
roundtrip(&state);
|
||||||
|
}
|
||||||
|
pw_log_debug("create result: %d", state.create_result);
|
||||||
|
if (state.create_result < 0) {
|
||||||
|
/* spa_strerror */
|
||||||
|
if (SPA_RESULT_IS_ASYNC(-state.create_result))
|
||||||
|
errno = EINPROGRESS;
|
||||||
|
else
|
||||||
|
errno = -state.create_result;
|
||||||
|
|
||||||
|
res = HAKUREI_PIPEWIRE_ATTACH;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
out:
|
||||||
|
if (listen_fd >= 0)
|
||||||
|
close(listen_fd);
|
||||||
|
if (state.sec != NULL)
|
||||||
|
pw_proxy_destroy((struct pw_proxy *)state.sec);
|
||||||
|
if (state.registry != NULL)
|
||||||
|
pw_proxy_destroy((struct pw_proxy *)state.registry);
|
||||||
|
if (state.core != NULL) {
|
||||||
|
/* these happen after core is checked non-NULL and always succeeds */
|
||||||
|
spa_hook_remove(&state.registry_listener);
|
||||||
|
spa_hook_remove(&state.core_listener);
|
||||||
|
|
||||||
|
pw_core_disconnect(state.core);
|
||||||
|
}
|
||||||
|
if (state.context != NULL)
|
||||||
|
pw_context_destroy(state.context);
|
||||||
|
if (state.loop != NULL)
|
||||||
|
pw_main_loop_destroy(state.loop);
|
||||||
|
pw_properties_free(state.props);
|
||||||
|
pw_deinit();
|
||||||
|
|
||||||
|
free((void *)socket_path);
|
||||||
|
if (remote_path != NULL)
|
||||||
|
free((void *)remote_path);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
40
internal/pipewire/pipewire-helper.h
Normal file
40
internal/pipewire/pipewire-helper.h
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#include <stdbool.h>
|
||||||
|
#include <sys/un.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
HAKUREI_PIPEWIRE_SUCCESS,
|
||||||
|
/* pw_main_loop_new failed, errno */
|
||||||
|
HAKUREI_PIPEWIRE_MAINLOOP,
|
||||||
|
/* pw_context_new failed, errno */
|
||||||
|
HAKUREI_PIPEWIRE_CTX,
|
||||||
|
/* pw_context_connect failed, errno */
|
||||||
|
HAKUREI_PIPEWIRE_CONNECT,
|
||||||
|
/* pw_core_get_registry failed */
|
||||||
|
HAKUREI_PIPEWIRE_REGISTRY,
|
||||||
|
/* no security context object found */
|
||||||
|
HAKUREI_PIPEWIRE_NOT_AVAIL,
|
||||||
|
/* socket failed, errno */
|
||||||
|
HAKUREI_PIPEWIRE_SOCKET,
|
||||||
|
/* bind failed, errno */
|
||||||
|
HAKUREI_PIPEWIRE_BIND,
|
||||||
|
/* listen failed, errno */
|
||||||
|
HAKUREI_PIPEWIRE_LISTEN,
|
||||||
|
/* pw_security_context_create failed, translated errno */
|
||||||
|
HAKUREI_PIPEWIRE_ATTACH,
|
||||||
|
|
||||||
|
/* ensure pathname failed, implemented in conn.go */
|
||||||
|
HAKUREI_PIPEWIRE_CREAT,
|
||||||
|
/* cleanup failed, implemented in conn.go */
|
||||||
|
HAKUREI_PIPEWIRE_CLEANUP,
|
||||||
|
} hakurei_pipewire_res;
|
||||||
|
|
||||||
|
hakurei_pipewire_res hakurei_pw_security_context_bind(
|
||||||
|
char *socket_path,
|
||||||
|
char *remote_path,
|
||||||
|
int close_fd);
|
||||||
|
|
||||||
|
/* returns whether the specified size fits in the sun_path field of sockaddr_un */
|
||||||
|
static inline bool hakurei_pw_is_valid_size_sun_path(size_t sz) {
|
||||||
|
struct sockaddr_un sockaddr;
|
||||||
|
return sz <= sizeof(sockaddr.sun_path);
|
||||||
|
};
|
||||||
166
internal/pipewire/pipewire.go
Normal file
166
internal/pipewire/pipewire.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
// Package pipewire implements the client side of PipeWire Security Context interface.
|
||||||
|
package pipewire
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo linux pkg-config: --static libpipewire-0.3
|
||||||
|
|
||||||
|
#include "pipewire-helper.h"
|
||||||
|
#include <pipewire/pipewire.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Version is the value of pw_get_headers_version().
|
||||||
|
Version = string(byte(C.PW_MAJOR+'0')) + "." + string(byte(C.PW_MINOR+'0')) + "." + string(byte(C.PW_MICRO+'0'))
|
||||||
|
|
||||||
|
// Remote is the environment with the remote name.
|
||||||
|
Remote = "PIPEWIRE_REMOTE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Res is the outcome of a call to [New].
|
||||||
|
Res = C.hakurei_pipewire_res
|
||||||
|
|
||||||
|
// An Error represents a failure during [New].
|
||||||
|
Error struct {
|
||||||
|
// Where the failure occurred.
|
||||||
|
Cause Res
|
||||||
|
// Attempted pathname socket.
|
||||||
|
Path string
|
||||||
|
// Global errno value set during the fault.
|
||||||
|
Errno error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// withPrefix returns prefix suffixed with errno description if available.
|
||||||
|
func (e *Error) withPrefix(prefix string) string {
|
||||||
|
if e.Errno == nil {
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
return prefix + ": " + e.Errno.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RSuccess is returned on a successful call.
|
||||||
|
RSuccess Res = C.HAKUREI_PIPEWIRE_SUCCESS
|
||||||
|
// RMainloop is returned if pw_main_loop_new failed. The global errno is set.
|
||||||
|
RMainloop Res = C.HAKUREI_PIPEWIRE_MAINLOOP
|
||||||
|
// RContext is returned if pw_context_new failed. The global errno is set.
|
||||||
|
RContext Res = C.HAKUREI_PIPEWIRE_CTX
|
||||||
|
// RConnect is returned if pw_context_connect failed. The global errno is set.
|
||||||
|
RConnect Res = C.HAKUREI_PIPEWIRE_CONNECT
|
||||||
|
// RRegistry is returned if pw_core_get_registry failed. The global errno is set.
|
||||||
|
RRegistry Res = C.HAKUREI_PIPEWIRE_REGISTRY
|
||||||
|
// RNotAvail is returned if no security context object found after roundtrip.
|
||||||
|
RNotAvail Res = C.HAKUREI_PIPEWIRE_NOT_AVAIL
|
||||||
|
// RSocket is returned if socket failed. The global errno is set.
|
||||||
|
RSocket Res = C.HAKUREI_PIPEWIRE_SOCKET
|
||||||
|
// RBind is returned if bind failed. The global errno is set.
|
||||||
|
RBind Res = C.HAKUREI_PIPEWIRE_BIND
|
||||||
|
// RListen is returned if listen failed. The global errno is set.
|
||||||
|
RListen Res = C.HAKUREI_PIPEWIRE_LISTEN
|
||||||
|
// RAttach is returned if pw_security_context_create failed.
|
||||||
|
// The internal create_result is translated and set as the global errno.
|
||||||
|
RAttach Res = C.HAKUREI_PIPEWIRE_ATTACH
|
||||||
|
|
||||||
|
// RCreate is returned if ensuring pathname availability failed. Returned by [New].
|
||||||
|
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) Message() string { return e.Error() }
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
switch e.Cause {
|
||||||
|
case RSuccess:
|
||||||
|
if e.Errno == nil {
|
||||||
|
return "success"
|
||||||
|
}
|
||||||
|
return e.Errno.Error()
|
||||||
|
|
||||||
|
case RMainloop:
|
||||||
|
return e.withPrefix("pw_main_loop_new failed")
|
||||||
|
case RContext:
|
||||||
|
return e.withPrefix("pw_context_new failed")
|
||||||
|
case RConnect:
|
||||||
|
return e.withPrefix("pw_context_connect failed")
|
||||||
|
case RRegistry:
|
||||||
|
return e.withPrefix("pw_core_get_registry failed")
|
||||||
|
case RNotAvail:
|
||||||
|
return "no security context object found"
|
||||||
|
|
||||||
|
case RSocket:
|
||||||
|
if e.Errno == nil {
|
||||||
|
return "socket operation failed"
|
||||||
|
}
|
||||||
|
return "socket: " + e.Errno.Error()
|
||||||
|
case RBind:
|
||||||
|
return e.withPrefix("cannot bind " + e.Path)
|
||||||
|
case RListen:
|
||||||
|
return e.withPrefix("cannot listen on " + e.Path)
|
||||||
|
|
||||||
|
case RAttach:
|
||||||
|
return e.withPrefix("pw_security_context_create failed")
|
||||||
|
|
||||||
|
case RCreate:
|
||||||
|
if e.Errno == nil {
|
||||||
|
return "cannot ensure pipewire pathname socket"
|
||||||
|
}
|
||||||
|
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:
|
||||||
|
return e.withPrefix("impossible outcome") /* not reached */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// securityContextBind calls hakurei_pw_security_context_bind.
|
||||||
|
//
|
||||||
|
// A non-nil error has concrete type [Error].
|
||||||
|
func securityContextBind(socketPath, remotePath string, closeFd int) error {
|
||||||
|
if hasNull(socketPath) || hasNull(remotePath) {
|
||||||
|
return &Error{Cause: RBind, Path: socketPath, Errno: errors.New("argument contains NUL character")}
|
||||||
|
}
|
||||||
|
if !C.hakurei_pw_is_valid_size_sun_path(C.size_t(len(socketPath))) {
|
||||||
|
return &Error{Cause: RBind, Path: socketPath, Errno: errors.New("socket pathname too long")}
|
||||||
|
}
|
||||||
|
|
||||||
|
var e Error
|
||||||
|
var remotePathP *C.char = nil
|
||||||
|
if remotePath != "" {
|
||||||
|
remotePathP = C.CString(remotePath)
|
||||||
|
}
|
||||||
|
e.Cause, e.Errno = C.hakurei_pw_security_context_bind(
|
||||||
|
C.CString(socketPath),
|
||||||
|
remotePathP,
|
||||||
|
C.int(closeFd),
|
||||||
|
)
|
||||||
|
if e.Cause == RSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
e.Path = socketPath
|
||||||
|
return &e
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasNull returns whether s contains the NUL character.
|
||||||
|
func hasNull(s string) bool { return strings.IndexByte(s, 0) > -1 }
|
||||||
157
internal/pipewire/pipewire_test.go
Normal file
157
internal/pipewire/pipewire_test.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
package pipewire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestError(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err Error
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"success", Error{
|
||||||
|
Cause: RSuccess,
|
||||||
|
}, "success"},
|
||||||
|
|
||||||
|
{"success errno", Error{
|
||||||
|
Cause: RSuccess,
|
||||||
|
Errno: stub.UniqueError(0),
|
||||||
|
}, "unique error 0 injected by the test suite"},
|
||||||
|
|
||||||
|
{"pw_main_loop_new", Error{
|
||||||
|
Cause: RMainloop,
|
||||||
|
Errno: stub.UniqueError(1),
|
||||||
|
}, "pw_main_loop_new failed: unique error 1 injected by the test suite"},
|
||||||
|
|
||||||
|
{"pw_context_new", Error{
|
||||||
|
Cause: RContext,
|
||||||
|
Errno: stub.UniqueError(2),
|
||||||
|
}, "pw_context_new failed: unique error 2 injected by the test suite"},
|
||||||
|
|
||||||
|
{"pw_context_connect", Error{
|
||||||
|
Cause: RConnect,
|
||||||
|
Errno: stub.UniqueError(3),
|
||||||
|
}, "pw_context_connect failed: unique error 3 injected by the test suite"},
|
||||||
|
|
||||||
|
{"pw_core_get_registry", Error{
|
||||||
|
Cause: RRegistry,
|
||||||
|
Errno: stub.UniqueError(4),
|
||||||
|
}, "pw_core_get_registry failed: unique error 4 injected by the test suite"},
|
||||||
|
|
||||||
|
{"not available", Error{
|
||||||
|
Cause: RNotAvail,
|
||||||
|
}, "no security context object found"},
|
||||||
|
|
||||||
|
{"not available errno", Error{
|
||||||
|
Cause: RNotAvail,
|
||||||
|
Errno: syscall.EAGAIN,
|
||||||
|
}, "no security context object found"},
|
||||||
|
|
||||||
|
{"socket", Error{
|
||||||
|
Cause: RSocket,
|
||||||
|
Errno: stub.UniqueError(5),
|
||||||
|
}, "socket: unique error 5 injected by the test suite"},
|
||||||
|
|
||||||
|
{"bind", Error{
|
||||||
|
Cause: RBind,
|
||||||
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire",
|
||||||
|
Errno: stub.UniqueError(6),
|
||||||
|
}, "cannot bind /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire: unique error 6 injected by the test suite"},
|
||||||
|
|
||||||
|
{"listen", Error{
|
||||||
|
Cause: RListen,
|
||||||
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire",
|
||||||
|
Errno: stub.UniqueError(7),
|
||||||
|
}, "cannot listen on /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire: unique error 7 injected by the test suite"},
|
||||||
|
|
||||||
|
{"socket invalid", Error{
|
||||||
|
Cause: RSocket,
|
||||||
|
}, "socket operation failed"},
|
||||||
|
|
||||||
|
{"pw_security_context_create", Error{
|
||||||
|
Cause: RAttach,
|
||||||
|
Errno: stub.UniqueError(8),
|
||||||
|
}, "pw_security_context_create failed: unique error 8 injected by the test suite"},
|
||||||
|
|
||||||
|
{"create", Error{
|
||||||
|
Cause: RCreate,
|
||||||
|
}, "cannot ensure pipewire pathname socket"},
|
||||||
|
|
||||||
|
{"create path", Error{
|
||||||
|
Cause: RCreate,
|
||||||
|
Errno: &os.PathError{Op: "create", Path: "/proc/nonexistent", Err: syscall.EEXIST},
|
||||||
|
}, "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{
|
||||||
|
Cause: 0xbad,
|
||||||
|
}, "impossible outcome"},
|
||||||
|
|
||||||
|
{"invalid errno", Error{
|
||||||
|
Cause: 0xbad,
|
||||||
|
Errno: stub.UniqueError(9),
|
||||||
|
}, "impossible outcome: unique error 9 injected by the test suite"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if got := tc.err.Message(); got != tc.want {
|
||||||
|
t.Errorf("Message: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSecurityContextBindValidate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("NUL", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
want := &Error{Cause: RBind, Path: "\x00", Errno: errors.New("argument contains NUL character")}
|
||||||
|
if got := securityContextBind("\x00", "\x00", -1); !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("securityContextBind: error = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("long", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// 256 bytes
|
||||||
|
const oversizedPath = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
|
||||||
|
want := &Error{Cause: RBind, Path: oversizedPath, Errno: errors.New("socket pathname too long")}
|
||||||
|
if got := securityContextBind(oversizedPath, "", -1); !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("securityContextBind: error = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@
|
|||||||
wayland,
|
wayland,
|
||||||
wayland-protocols,
|
wayland-protocols,
|
||||||
wayland-scanner,
|
wayland-scanner,
|
||||||
|
pipewire,
|
||||||
xorg,
|
xorg,
|
||||||
|
|
||||||
# for hpkg
|
# for hpkg
|
||||||
@ -94,6 +95,7 @@ buildGoModule rec {
|
|||||||
libseccomp
|
libseccomp
|
||||||
acl
|
acl
|
||||||
wayland
|
wayland
|
||||||
|
pipewire
|
||||||
]
|
]
|
||||||
++ (with xorg; [
|
++ (with xorg; [
|
||||||
libxcb
|
libxcb
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user