diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index 7ce51f4..c995626 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -28,12 +28,12 @@ func printShowSystem(output io.Writer, short, flagJSON bool) { return } - t.Printf("Version:\t%s (libwayland %s)\n", hi.Version, hi.WaylandVersion) t.Printf("User:\t%d\n", hi.User) t.Printf("TempDir:\t%s\n", hi.TempDir) t.Printf("SharePath:\t%s\n", hi.SharePath) t.Printf("RuntimePath:\t%s\n", hi.RuntimePath) 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. diff --git a/hst/hst.go b/hst/hst.go index f48b9f8..2d77371 100644 --- a/hst/hst.go +++ b/hst/hst.go @@ -56,6 +56,8 @@ type Paths struct { type Info struct { // WaylandVersion is the libwayland value of 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 string `json:"version"` diff --git a/internal/outcome/outcome.go b/internal/outcome/outcome.go index e256355..fa50ed4 100644 --- a/internal/outcome/outcome.go +++ b/internal/outcome/outcome.go @@ -12,6 +12,7 @@ import ( "hakurei.app/internal/acl" "hakurei.app/internal/env" "hakurei.app/internal/info" + "hakurei.app/internal/pipewire" "hakurei.app/internal/system" "hakurei.app/internal/wayland" "hakurei.app/message" @@ -21,7 +22,7 @@ import ( // // This must not be called from within package outcome. 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)} env.CopyPaths().Copy(&hi.Paths, hi.User) return &hi diff --git a/internal/pipewire/conn.go b/internal/pipewire/conn.go new file mode 100644 index 0000000..dc521e4 --- /dev/null +++ b/internal/pipewire/conn.go @@ -0,0 +1,71 @@ +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 +} + +// Close releases any resources held by [SecurityContext], and prevents further +// connections to its associated socket. +func (sc *SecurityContext) Close() error { + if sc == nil { + return os.ErrInvalid + } + return errors.Join( + syscall.Close(sc.closeFds[1]), + syscall.Close(sc.closeFds[0]), + ) +} + +// 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, + syscall.Close(closeFds[1]), + syscall.Close(closeFds[0]), + ) + } else { + return &SecurityContext{closeFds}, nil + } +} diff --git a/internal/pipewire/conn_test.go b/internal/pipewire/conn_test.go new file mode 100644 index 0000000..08d6a88 --- /dev/null +++ b/internal/pipewire/conn_test.go @@ -0,0 +1,54 @@ +package pipewire + +import ( + "errors" + "os" + "reflect" + "syscall" + "testing" + + "hakurei.app/container/check" +) + +func TestSecurityContextClose(t *testing.T) { + t.Parallel() + + 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) + } + 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) + } + + wantErr := errors.Join(syscall.EBADF, syscall.EBADF) + 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) + } +} diff --git a/internal/pipewire/pipewire-helper.c b/internal/pipewire/pipewire-helper.c new file mode 100644 index 0000000..2766c48 --- /dev/null +++ b/internal/pipewire/pipewire-helper.c @@ -0,0 +1,252 @@ +#include "pipewire-helper.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/* 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; +} diff --git a/internal/pipewire/pipewire-helper.h b/internal/pipewire/pipewire-helper.h new file mode 100644 index 0000000..9c37b9b --- /dev/null +++ b/internal/pipewire/pipewire-helper.h @@ -0,0 +1,38 @@ +#include +#include + +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, +} 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); +}; diff --git a/internal/pipewire/pipewire.go b/internal/pipewire/pipewire.go new file mode 100644 index 0000000..08dbd4d --- /dev/null +++ b/internal/pipewire/pipewire.go @@ -0,0 +1,148 @@ +// 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 +*/ +import "C" +import ( + "errors" + "strings" +) + +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 +) + +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() + + 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 } diff --git a/internal/pipewire/pipewire_test.go b/internal/pipewire/pipewire_test.go new file mode 100644 index 0000000..d298e34 --- /dev/null +++ b/internal/pipewire/pipewire_test.go @@ -0,0 +1,136 @@ +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: "/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire", + Errno: stub.UniqueError(6), + }, "cannot bind /hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire: unique error 6 injected by the test suite"}, + + {"listen", Error{ + Cause: RListen, + Path: "/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/pipewire", + Errno: stub.UniqueError(7), + }, "cannot listen on /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"}, + + {"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) + } + }) +} diff --git a/package.nix b/package.nix index fb449c1..d8c4ebf 100644 --- a/package.nix +++ b/package.nix @@ -11,6 +11,7 @@ wayland, wayland-protocols, wayland-scanner, + pipewire, xorg, # for hpkg @@ -94,6 +95,7 @@ buildGoModule rec { libseccomp acl wayland + pipewire ] ++ (with xorg; [ libxcb