Compare commits
3 Commits
2a6c8ba51b
...
81430987e7
| Author | SHA1 | Date | |
|---|---|---|---|
| 81430987e7 | |||
| 5af08cb9bf | |||
| aab92ce3c1 |
@ -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.
|
||||||
|
|||||||
2
dist/release.sh
vendored
2
dist/release.sh
vendored
@ -9,7 +9,7 @@ cp -v "README.md" "dist/hsurc.default" "dist/install.sh" "${out}"
|
|||||||
cp -rv "dist/comp" "${out}"
|
cp -rv "dist/comp" "${out}"
|
||||||
|
|
||||||
go generate ./...
|
go generate ./...
|
||||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
|
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid=''
|
||||||
-X hakurei.app/internal/info.buildVersion=${VERSION}
|
-X hakurei.app/internal/info.buildVersion=${VERSION}
|
||||||
-X hakurei.app/internal/info.hakureiPath=/usr/bin/hakurei
|
-X hakurei.app/internal/info.hakureiPath=/usr/bin/hakurei
|
||||||
-X hakurei.app/internal/info.hsuPath=/usr/bin/hsu
|
-X hakurei.app/internal/info.hsuPath=/usr/bin/hsu
|
||||||
|
|||||||
@ -110,7 +110,7 @@
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = hakurei;
|
default = hakurei;
|
||||||
hakurei = pkgs.pkgsStatic.callPackage ./package.nix {
|
hakurei = pkgs.callPackage ./package.nix {
|
||||||
inherit (pkgs)
|
inherit (pkgs)
|
||||||
# passthru.buildInputs
|
# passthru.buildInputs
|
||||||
go
|
go
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
71
internal/pipewire/conn.go
Normal file
71
internal/pipewire/conn.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
54
internal/pipewire/conn_test.go
Normal file
54
internal/pipewire/conn_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
38
internal/pipewire/pipewire-helper.h
Normal file
38
internal/pipewire/pipewire-helper.h
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#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,
|
||||||
|
} 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);
|
||||||
|
};
|
||||||
148
internal/pipewire/pipewire.go
Normal file
148
internal/pipewire/pipewire.go
Normal file
@ -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 <pipewire/pipewire.h>
|
||||||
|
*/
|
||||||
|
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 }
|
||||||
136
internal/pipewire/pipewire_test.go
Normal file
136
internal/pipewire/pipewire_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ package system
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
|
|
||||||
"hakurei.app/container/check"
|
"hakurei.app/container/check"
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
@ -63,9 +62,6 @@ func (w *waylandOp) revert(sys *I, _ *Criteria) error {
|
|||||||
if w.ctx != nil {
|
if w.ctx != nil {
|
||||||
hangupErr = w.ctx.Close()
|
hangupErr = w.ctx.Close()
|
||||||
}
|
}
|
||||||
if err := sys.remove(w.dst.String()); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
||||||
removeErr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
return newOpError("wayland", errors.Join(hangupErr, removeErr), true)
|
return newOpError("wayland", errors.Join(hangupErr, removeErr), true)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,21 +36,6 @@ func TestWaylandOp(t *testing.T) {
|
|||||||
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(2)),
|
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(2)),
|
||||||
}, &OpError{Op: "wayland", Err: errors.Join(stub.UniqueError(2), os.ErrInvalid)}, nil, nil},
|
}, &OpError{Op: "wayland", Err: errors.Join(stub.UniqueError(2), os.ErrInvalid)}, nil, nil},
|
||||||
|
|
||||||
{"remove", 0xbeef, 0xff, &waylandOp{nil,
|
|
||||||
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"),
|
|
||||||
m("/run/user/1971/wayland-0"),
|
|
||||||
"org.chromium.Chromium",
|
|
||||||
"ebf083d1b175911782d413369b64ce7c",
|
|
||||||
}, []stub.Call{
|
|
||||||
call("waylandNew", stub.ExpectArgs{m("/run/user/1971/wayland-0"), m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}, nil, nil),
|
|
||||||
call("verbosef", stub.ExpectArgs{"wayland pathname socket on %q via %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0")}}, nil, nil),
|
|
||||||
call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil),
|
|
||||||
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
|
||||||
}, nil, []stub.Call{
|
|
||||||
call("verbosef", stub.ExpectArgs{"hanging up wayland socket on %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland")}}, nil, nil),
|
|
||||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}, nil, stub.UniqueError(1)),
|
|
||||||
}, &OpError{Op: "wayland", Err: errors.Join(stub.UniqueError(1)), Revert: true}},
|
|
||||||
|
|
||||||
{"success", 0xbeef, 0xff, &waylandOp{nil,
|
{"success", 0xbeef, 0xff, &waylandOp{nil,
|
||||||
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"),
|
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"),
|
||||||
m("/run/user/1971/wayland-0"),
|
m("/run/user/1971/wayland-0"),
|
||||||
@ -63,7 +48,6 @@ func TestWaylandOp(t *testing.T) {
|
|||||||
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("verbosef", stub.ExpectArgs{"hanging up wayland socket on %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland")}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"hanging up wayland socket on %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland")}}, nil, nil),
|
||||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}, nil, nil),
|
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -12,18 +12,32 @@ import (
|
|||||||
type SecurityContext struct {
|
type SecurityContext struct {
|
||||||
// Pipe with its write end passed to security-context-v1.
|
// Pipe with its write end passed to security-context-v1.
|
||||||
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 wayland 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 Wayland display at displayPath
|
// New creates a new security context on the Wayland display at displayPath
|
||||||
@ -51,12 +65,19 @@ func New(displayPath, bindPath *check.Absolute, appID, instanceID string) (*Secu
|
|||||||
} else {
|
} else {
|
||||||
closeFds, bindErr := securityContextBindPipe(fd, bindPath, appID, instanceID)
|
closeFds, bindErr := securityContextBindPipe(fd, bindPath, appID, instanceID)
|
||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
|
// securityContextBindPipe does not try to remove the socket during cleanup
|
||||||
|
closeErr := os.Remove(bindPath.String())
|
||||||
|
if closeErr != nil && errors.Is(closeErr, os.ErrNotExist) {
|
||||||
|
closeErr = nil
|
||||||
|
}
|
||||||
|
|
||||||
err = errors.Join(bindErr, // already wrapped
|
err = errors.Join(bindErr, // already wrapped
|
||||||
|
closeErr,
|
||||||
// do not leak the socket
|
// do not leak the socket
|
||||||
syscall.Close(fd),
|
syscall.Close(fd),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return &SecurityContext{closeFds}, err
|
return &SecurityContext{closeFds, bindPath}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package wayland
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
@ -11,13 +12,18 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ctx SecurityContext
|
var ctx SecurityContext
|
||||||
|
if f, err := os.Create(path.Join(t.TempDir(), "remove")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
ctx.bindPath = check.MustAbs(f.Name())
|
||||||
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -25,9 +31,15 @@ func TestSecurityContextClose(t *testing.T) {
|
|||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@ typedef enum {
|
|||||||
HAKUREI_WAYLAND_HOST_SOCKET,
|
HAKUREI_WAYLAND_HOST_SOCKET,
|
||||||
/* connect for host server failed, implemented in conn.go */
|
/* connect for host server failed, implemented in conn.go */
|
||||||
HAKUREI_WAYLAND_HOST_CONNECT,
|
HAKUREI_WAYLAND_HOST_CONNECT,
|
||||||
|
/* cleanup failed, implemented in conn.go */
|
||||||
|
HAKUREI_WAYLAND_CLEANUP,
|
||||||
} hakurei_wayland_res;
|
} hakurei_wayland_res;
|
||||||
|
|
||||||
hakurei_wayland_res hakurei_security_context_bind(
|
hakurei_wayland_res hakurei_security_context_bind(
|
||||||
|
|||||||
@ -14,7 +14,9 @@ package wayland
|
|||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -83,6 +85,9 @@ const (
|
|||||||
RHostSocket Res = C.HAKUREI_WAYLAND_HOST_SOCKET
|
RHostSocket Res = C.HAKUREI_WAYLAND_HOST_SOCKET
|
||||||
// RHostConnect is returned if connect failed for host server. Returned by [New].
|
// RHostConnect is returned if connect failed for host server. Returned by [New].
|
||||||
RHostConnect Res = C.HAKUREI_WAYLAND_HOST_CONNECT
|
RHostConnect Res = C.HAKUREI_WAYLAND_HOST_CONNECT
|
||||||
|
|
||||||
|
// RCleanup is returned if cleanup fails. Returned by [SecurityContext.Close].
|
||||||
|
RCleanup Res = C.HAKUREI_WAYLAND_CLEANUP
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *Error) Unwrap() error { return e.Errno }
|
func (e *Error) Unwrap() error { return e.Errno }
|
||||||
@ -124,6 +129,19 @@ func (e *Error) Error() string {
|
|||||||
case RHostConnect:
|
case RHostConnect:
|
||||||
return e.withPrefix("cannot connect to " + e.Host)
|
return e.withPrefix("cannot connect to " + e.Host)
|
||||||
|
|
||||||
|
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 wayland close_fd pipe: " + errno.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.withPrefix("cannot hang up wayland security_context")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return e.withPrefix("impossible outcome") /* not reached */
|
return e.withPrefix("impossible outcome") /* not reached */
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,15 +58,15 @@ func TestError(t *testing.T) {
|
|||||||
|
|
||||||
{"bind", Error{
|
{"bind", Error{
|
||||||
Cause: RBind,
|
Cause: RBind,
|
||||||
Path: "/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
||||||
Errno: stub.UniqueError(5),
|
Errno: stub.UniqueError(5),
|
||||||
}, "cannot bind /hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 5 injected by the test suite"},
|
}, "cannot bind /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 5 injected by the test suite"},
|
||||||
|
|
||||||
{"listen", Error{
|
{"listen", Error{
|
||||||
Cause: RListen,
|
Cause: RListen,
|
||||||
Path: "/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
||||||
Errno: stub.UniqueError(6),
|
Errno: stub.UniqueError(6),
|
||||||
}, "cannot listen on /hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 6 injected by the test suite"},
|
}, "cannot listen on /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 6 injected by the test suite"},
|
||||||
|
|
||||||
{"socket invalid", Error{
|
{"socket invalid", Error{
|
||||||
Cause: RSocket,
|
Cause: RSocket,
|
||||||
@ -92,6 +92,27 @@ func TestError(t *testing.T) {
|
|||||||
Errno: stub.UniqueError(8),
|
Errno: stub.UniqueError(8),
|
||||||
}, "cannot connect to /run/user/1971/wayland-1: unique error 8 injected by the test suite"},
|
}, "cannot connect to /run/user/1971/wayland-1: unique error 8 injected by the test suite"},
|
||||||
|
|
||||||
|
{"cleanup", Error{
|
||||||
|
Cause: RCleanup,
|
||||||
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
||||||
|
}, "cannot hang up wayland security_context"},
|
||||||
|
|
||||||
|
{"cleanup PathError", Error{
|
||||||
|
Cause: RCleanup,
|
||||||
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
||||||
|
Errno: errors.Join(syscall.EINVAL, &os.PathError{
|
||||||
|
Op: "remove",
|
||||||
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
||||||
|
Err: stub.UniqueError(9),
|
||||||
|
}),
|
||||||
|
}, "remove /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 9 injected by the test suite"},
|
||||||
|
|
||||||
|
{"cleanup errno", Error{
|
||||||
|
Cause: RCleanup,
|
||||||
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
||||||
|
Errno: errors.Join(syscall.EINVAL),
|
||||||
|
}, "cannot close wayland close_fd pipe: invalid argument"},
|
||||||
|
|
||||||
{"invalid", Error{
|
{"invalid", Error{
|
||||||
Cause: 0xbad,
|
Cause: 0xbad,
|
||||||
}, "impossible outcome"},
|
}, "impossible outcome"},
|
||||||
|
|||||||
@ -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