Compare commits
33 Commits
v0.3.1
...
df389e239f
| Author | SHA1 | Date | |
|---|---|---|---|
|
df389e239f
|
|||
|
5af08cb9bf
|
|||
|
aab92ce3c1
|
|||
|
a495e09a8f
|
|||
|
3afca2bd5b
|
|||
|
b73a789dfe
|
|||
|
38b5ff0cec
|
|||
|
3c204b9b40
|
|||
|
00771efeb4
|
|||
|
61972d61f6
|
|||
|
fe40af7b7e
|
|||
|
12751932d1
|
|||
|
41b49137a8
|
|||
|
c761e1de4d
|
|||
|
a91920310d
|
|||
|
16e674782a
|
|||
|
47244daefb
|
|||
|
46fa104419
|
|||
|
45953b3d9c
|
|||
|
42759e7a9f
|
|||
|
8e2d2c8246
|
|||
|
299685775a
|
|||
|
b7406cc4c4
|
|||
|
690a0ed0d6
|
|||
|
a9d72a5eb1
|
|||
|
6d14bb814f
|
|||
|
be0e387ab0
|
|||
|
abeb67964f
|
|||
|
bf5d10743f
|
|||
|
4e7aab07d5
|
|||
|
15a66a2b31
|
|||
|
f347d44c22
|
|||
|
b5630f6883
|
2
.clang-format
Normal file
2
.clang-format
Normal file
@@ -0,0 +1,2 @@
|
||||
ColumnLimit: 0
|
||||
IndentWidth: 4
|
||||
@@ -11,21 +11,24 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
_ "unsafe"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/env"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/outcome"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
|
||||
// if it is not nil, or the original value if it is.
|
||||
//
|
||||
//go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap
|
||||
func optionalErrorUnwrap(_ error) error
|
||||
func optionalErrorUnwrap(err error) error
|
||||
|
||||
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
|
||||
var (
|
||||
@@ -350,7 +353,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||
}
|
||||
|
||||
c.Command("version", "Display version information", func(args []string) error { fmt.Println(internal.Version()); return errSuccess })
|
||||
c.Command("version", "Display version information", func(args []string) error { fmt.Println(info.Version()); return errSuccess })
|
||||
c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
|
||||
c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess })
|
||||
c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
package main_test
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
_ "unsafe"
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
)
|
||||
|
||||
//go:linkname decodeJSON hakurei.app/cmd/hakurei.decodeJSON
|
||||
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any)
|
||||
|
||||
func TestDecodeJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -62,9 +57,6 @@ func TestDecodeJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
//go:linkname encodeJSON hakurei.app/cmd/hakurei.encodeJSON
|
||||
func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any)
|
||||
|
||||
func TestEncodeJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -74,7 +66,7 @@ func TestEncodeJSON(t *testing.T) {
|
||||
want string
|
||||
}{
|
||||
{"marshaler", errorJSONMarshaler{},
|
||||
`cannot encode json for main_test.errorJSONMarshaler: unique error 3735928559 injected by the test suite`},
|
||||
`cannot encode json for main.errorJSONMarshaler: unique error 3735928559 injected by the test suite`},
|
||||
{"default", func() {},
|
||||
`cannot write json: json: unsupported type: func()`},
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/env"
|
||||
"hakurei.app/internal/outcome"
|
||||
"hakurei.app/internal/store"
|
||||
"hakurei.app/message"
|
||||
@@ -23,21 +21,19 @@ import (
|
||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
info := &hst.Info{Version: internal.Version(), User: new(outcome.Hsu).MustID(nil)}
|
||||
env.CopyPaths().Copy(&info.Paths, info.User)
|
||||
hi := outcome.Info()
|
||||
|
||||
if flagJSON {
|
||||
encodeJSON(log.Fatal, output, short, info)
|
||||
encodeJSON(log.Fatal, output, short, hi)
|
||||
return
|
||||
}
|
||||
|
||||
t.Printf("Version:\t%s\n", info.Version)
|
||||
t.Printf("User:\t%d\n", info.User)
|
||||
t.Printf("TempDir:\t%s\n", info.TempDir)
|
||||
t.Printf("SharePath:\t%s\n", info.SharePath)
|
||||
t.Printf("RuntimePath:\t%s\n", info.RuntimePath)
|
||||
t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
|
||||
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.
|
||||
@@ -90,12 +86,6 @@ func printShowInstance(
|
||||
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
|
||||
}
|
||||
if config.Container != nil {
|
||||
if config.Container.Home != nil {
|
||||
t.Printf(" Home:\t%s\n", config.Container.Home)
|
||||
}
|
||||
if config.Container.Hostname != "" {
|
||||
t.Printf(" Hostname:\t%s\n", config.Container.Hostname)
|
||||
}
|
||||
flags := config.Container.Flags.String()
|
||||
|
||||
// this is included in the upper hst.Config struct but is relevant here
|
||||
@@ -110,6 +100,12 @@ func printShowInstance(
|
||||
}
|
||||
t.Printf(" Flags:\t%s\n", flags)
|
||||
|
||||
if config.Container.Home != nil {
|
||||
t.Printf(" Home:\t%s\n", config.Container.Home)
|
||||
}
|
||||
if config.Container.Hostname != "" {
|
||||
t.Printf(" Hostname:\t%s\n", config.Container.Hostname)
|
||||
}
|
||||
if config.Container.Path != nil {
|
||||
t.Printf(" Path:\t%s\n", config.Container.Path)
|
||||
}
|
||||
|
||||
@@ -64,9 +64,9 @@ func TestPrintShowInstance(t *testing.T) {
|
||||
Identity: 9 (org.chromium.Chromium)
|
||||
Enablements: wayland, dbus, pulseaudio
|
||||
Groups: video, dialout, plugdev
|
||||
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
||||
Home: /data/data/org.chromium.Chromium
|
||||
Hostname: localhost
|
||||
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
||||
Path: /run/current-system/sw/bin/chromium
|
||||
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
|
||||
|
||||
@@ -161,9 +161,9 @@ App
|
||||
Identity: 9 (org.chromium.Chromium)
|
||||
Enablements: wayland, dbus, pulseaudio
|
||||
Groups: video, dialout, plugdev
|
||||
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
||||
Home: /data/data/org.chromium.Chromium
|
||||
Hostname: localhost
|
||||
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
|
||||
Path: /run/current-system/sw/bin/chromium
|
||||
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
"os/exec"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
var hakureiPathVal = internal.MustHakureiPath().String()
|
||||
var hakureiPathVal = info.MustHakureiPath().String()
|
||||
|
||||
func mustRunApp(ctx context.Context, msg message.Msg, config *hst.Config, beforeFail func()) {
|
||||
var (
|
||||
|
||||
@@ -56,7 +56,7 @@ func NewAbs(pathname string) (*Absolute, error) {
|
||||
// MustAbs calls [NewAbs] and panics on error.
|
||||
func MustAbs(pathname string) *Absolute {
|
||||
if a, err := NewAbs(pathname); err != nil {
|
||||
panic(err.Error())
|
||||
panic(err)
|
||||
} else {
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ import (
|
||||
. "hakurei.app/container/check"
|
||||
)
|
||||
|
||||
// unsafeAbs returns check.Absolute on any string value.
|
||||
//
|
||||
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
|
||||
func unsafeAbs(_ string) *Absolute
|
||||
func unsafeAbs(pathname string) *Absolute
|
||||
|
||||
func TestAbsoluteError(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -82,9 +84,9 @@ func TestNewAbs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
defer func() {
|
||||
wantPanic := `path "etc" is not absolute`
|
||||
wantPanic := &AbsoluteError{Pathname: "etc"}
|
||||
|
||||
if r := recover(); r != wantPanic {
|
||||
if r := recover(); !reflect.DeepEqual(r, wantPanic) {
|
||||
t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/container/vfs"
|
||||
@@ -29,6 +30,45 @@ import (
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// Note: this package requires cgo, which is unavailable in the Go playground.
|
||||
func Example() {
|
||||
// Must be called early if the current process starts containers.
|
||||
container.TryArgv0(nil)
|
||||
|
||||
// Configure the container.
|
||||
z := container.New(context.Background(), nil)
|
||||
z.Hostname = "hakurei-example"
|
||||
z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
|
||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
|
||||
// Bind / for demonstration.
|
||||
z.Bind(fhs.AbsRoot, fhs.AbsRoot, 0)
|
||||
if name, err := exec.LookPath("hostname"); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
z.Path = check.MustAbs(name)
|
||||
}
|
||||
|
||||
// This completes the first stage of container setup and starts the container init process.
|
||||
// The new process blocks until the Serve method is called.
|
||||
if err := z.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// This serves the setup payload to the container init process,
|
||||
// starting the second stage of container setup.
|
||||
if err := z.Serve(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Must be called if the Start method succeeds.
|
||||
if err := z.Wait(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Output: hakurei-example
|
||||
}
|
||||
|
||||
func TestStartError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -722,12 +762,14 @@ func TestMain(m *testing.M) {
|
||||
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
|
||||
msg := message.New(nil)
|
||||
msg.SwapVerbose(testing.Verbose())
|
||||
executable := check.MustAbs(container.MustExecutable(msg))
|
||||
|
||||
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
|
||||
c.Env = append(c.Env, envDoCheck+"=1")
|
||||
c.Bind(check.MustAbs(os.Args[0]), absHelperInnerPath, 0)
|
||||
c.Bind(executable, absHelperInnerPath, 0)
|
||||
|
||||
// in case test has cgo enabled
|
||||
if entries, err := ldd.Exec(ctx, msg, os.Args[0]); err != nil {
|
||||
if entries, err := ldd.Resolve(ctx, msg, executable); err != nil {
|
||||
log.Fatalf("ldd: %v", err)
|
||||
} else {
|
||||
*libPaths = ldd.Path(entries)
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
|
||||
/* constants in this file bypass abs check, be extremely careful when changing them! */
|
||||
|
||||
// unsafeAbs returns check.Absolute on any string value.
|
||||
//
|
||||
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
|
||||
func unsafeAbs(_ string) *check.Absolute
|
||||
func unsafeAbs(pathname string) *check.Absolute
|
||||
|
||||
var (
|
||||
// AbsRoot is [Root] as [check.Absolute].
|
||||
@@ -34,6 +36,8 @@ var (
|
||||
|
||||
// AbsDev is [Dev] as [check.Absolute].
|
||||
AbsDev = unsafeAbs(Dev)
|
||||
// AbsDevShm is [DevShm] as [check.Absolute].
|
||||
AbsDevShm = unsafeAbs(DevShm)
|
||||
// AbsProc is [Proc] as [check.Absolute].
|
||||
AbsProc = unsafeAbs(Proc)
|
||||
// AbsSys is [Sys] as [check.Absolute].
|
||||
|
||||
@@ -29,6 +29,8 @@ const (
|
||||
|
||||
// Dev points to the root directory for device nodes.
|
||||
Dev = "/dev/"
|
||||
// DevShm is the place for POSIX shared memory segments, as created via shm_open(3).
|
||||
DevShm = "/dev/shm/"
|
||||
// Proc points to a virtual kernel file system exposing the process list and other functionality.
|
||||
Proc = "/proc/"
|
||||
// ProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
|
||||
|
||||
@@ -9,136 +9,130 @@
|
||||
|
||||
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
|
||||
|
||||
int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
|
||||
uint32_t arch, uint32_t multiarch,
|
||||
struct hakurei_syscall_rule *rules,
|
||||
size_t rules_sz, hakurei_export_flag flags) {
|
||||
int i;
|
||||
int last_allowed_family;
|
||||
int disallowed;
|
||||
struct hakurei_syscall_rule *rule;
|
||||
void *buf;
|
||||
size_t len = 0;
|
||||
int32_t hakurei_scmp_make_filter(
|
||||
int *ret_p, uintptr_t allocate_p,
|
||||
uint32_t arch, uint32_t multiarch,
|
||||
struct hakurei_syscall_rule *rules,
|
||||
size_t rules_sz, hakurei_export_flag flags) {
|
||||
int i;
|
||||
int last_allowed_family;
|
||||
int disallowed;
|
||||
struct hakurei_syscall_rule *rule;
|
||||
void *buf;
|
||||
size_t len = 0;
|
||||
|
||||
int32_t res = 0; /* refer to resPrefix for message */
|
||||
int32_t res = 0; /* refer to resPrefix for message */
|
||||
|
||||
/* Blocklist all but unix, inet, inet6 and netlink */
|
||||
struct {
|
||||
int family;
|
||||
hakurei_export_flag flags_mask;
|
||||
} socket_family_allowlist[] = {
|
||||
/* NOTE: Keep in numerical order */
|
||||
{AF_UNSPEC, 0},
|
||||
{AF_LOCAL, 0},
|
||||
{AF_INET, 0},
|
||||
{AF_INET6, 0},
|
||||
{AF_NETLINK, 0},
|
||||
{AF_CAN, HAKUREI_EXPORT_CAN},
|
||||
{AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
|
||||
};
|
||||
/* Blocklist all but unix, inet, inet6 and netlink */
|
||||
struct {
|
||||
int family;
|
||||
hakurei_export_flag flags_mask;
|
||||
} socket_family_allowlist[] = {
|
||||
/* NOTE: Keep in numerical order */
|
||||
{AF_UNSPEC, 0},
|
||||
{AF_LOCAL, 0},
|
||||
{AF_INET, 0},
|
||||
{AF_INET6, 0},
|
||||
{AF_NETLINK, 0},
|
||||
{AF_CAN, HAKUREI_EXPORT_CAN},
|
||||
{AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
|
||||
};
|
||||
|
||||
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
|
||||
if (ctx == NULL) {
|
||||
res = 1;
|
||||
goto out;
|
||||
} else
|
||||
errno = 0;
|
||||
|
||||
/* We only really need to handle arches on multiarch systems.
|
||||
* If only one arch is supported the default is fine */
|
||||
if (arch != 0) {
|
||||
/* This *adds* the target arch, instead of replacing the
|
||||
* native one. This is not ideal, because we'd like to only
|
||||
* allow the target arch, but we can't really disallow the
|
||||
* native arch at this point, because then bubblewrap
|
||||
* couldn't continue running. */
|
||||
*ret_p = seccomp_arch_add(ctx, arch);
|
||||
if (*ret_p < 0 && *ret_p != -EEXIST) {
|
||||
res = 2;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) {
|
||||
*ret_p = seccomp_arch_add(ctx, multiarch);
|
||||
if (*ret_p < 0 && *ret_p != -EEXIST) {
|
||||
res = 3;
|
||||
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
|
||||
if (ctx == NULL) {
|
||||
res = 1;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else
|
||||
errno = 0;
|
||||
|
||||
for (i = 0; i < rules_sz; i++) {
|
||||
rule = &rules[i];
|
||||
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
|
||||
/* We only really need to handle arches on multiarch systems.
|
||||
* If only one arch is supported the default is fine */
|
||||
if (arch != 0) {
|
||||
/* This *adds* the target arch, instead of replacing the
|
||||
* native one. This is not ideal, because we'd like to only
|
||||
* allow the target arch, but we can't really disallow the
|
||||
* native arch at this point, because then bubblewrap
|
||||
* couldn't continue running. */
|
||||
*ret_p = seccomp_arch_add(ctx, arch);
|
||||
if (*ret_p < 0 && *ret_p != -EEXIST) {
|
||||
res = 2;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (rule->arg)
|
||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
|
||||
rule->syscall, 1, *rule->arg);
|
||||
else
|
||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
|
||||
rule->syscall, 0);
|
||||
|
||||
if (*ret_p == -EFAULT) {
|
||||
res = 4;
|
||||
goto out;
|
||||
} else if (*ret_p < 0) {
|
||||
res = 5;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
/* Socket filtering doesn't work on e.g. i386, so ignore failures here
|
||||
* However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
|
||||
* something else: https://github.com/seccomp/libseccomp/issues/8 */
|
||||
last_allowed_family = -1;
|
||||
for (i = 0; i < LEN(socket_family_allowlist); i++) {
|
||||
if (socket_family_allowlist[i].flags_mask != 0 &&
|
||||
(socket_family_allowlist[i].flags_mask & flags) !=
|
||||
socket_family_allowlist[i].flags_mask)
|
||||
continue;
|
||||
|
||||
for (disallowed = last_allowed_family + 1;
|
||||
disallowed < socket_family_allowlist[i].family; disallowed++) {
|
||||
/* Blocklist the in-between valid families */
|
||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT),
|
||||
SCMP_SYS(socket), 1,
|
||||
SCMP_A0(SCMP_CMP_EQ, disallowed));
|
||||
}
|
||||
last_allowed_family = socket_family_allowlist[i].family;
|
||||
}
|
||||
/* Blocklist the rest */
|
||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1,
|
||||
SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
|
||||
|
||||
if (allocate_p == 0) {
|
||||
*ret_p = seccomp_load(ctx);
|
||||
if (*ret_p != 0) {
|
||||
res = 7;
|
||||
goto out;
|
||||
}
|
||||
} else {
|
||||
*ret_p = seccomp_export_bpf_mem(ctx, NULL, &len);
|
||||
if (*ret_p != 0) {
|
||||
res = 6;
|
||||
goto out;
|
||||
if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) {
|
||||
*ret_p = seccomp_arch_add(ctx, multiarch);
|
||||
if (*ret_p < 0 && *ret_p != -EEXIST) {
|
||||
res = 3;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buf = hakurei_scmp_allocate(allocate_p, len);
|
||||
if (buf == NULL) {
|
||||
res = 4;
|
||||
goto out;
|
||||
for (i = 0; i < rules_sz; i++) {
|
||||
rule = &rules[i];
|
||||
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
|
||||
|
||||
if (rule->arg)
|
||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 1, *rule->arg);
|
||||
else
|
||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 0);
|
||||
|
||||
if (*ret_p == -EFAULT) {
|
||||
res = 4;
|
||||
goto out;
|
||||
} else if (*ret_p < 0) {
|
||||
res = 5;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
*ret_p = seccomp_export_bpf_mem(ctx, buf, &len);
|
||||
if (*ret_p != 0) {
|
||||
res = 6;
|
||||
goto out;
|
||||
/* Socket filtering doesn't work on e.g. i386, so ignore failures here
|
||||
* However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
|
||||
* something else: https://github.com/seccomp/libseccomp/issues/8 */
|
||||
last_allowed_family = -1;
|
||||
for (i = 0; i < LEN(socket_family_allowlist); i++) {
|
||||
if (socket_family_allowlist[i].flags_mask != 0 &&
|
||||
(socket_family_allowlist[i].flags_mask & flags) != socket_family_allowlist[i].flags_mask)
|
||||
continue;
|
||||
|
||||
for (disallowed = last_allowed_family + 1; disallowed < socket_family_allowlist[i].family; disallowed++) {
|
||||
/* Blocklist the in-between valid families */
|
||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, disallowed));
|
||||
}
|
||||
last_allowed_family = socket_family_allowlist[i].family;
|
||||
}
|
||||
/* Blocklist the rest */
|
||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
|
||||
|
||||
if (allocate_p == 0) {
|
||||
*ret_p = seccomp_load(ctx);
|
||||
if (*ret_p != 0) {
|
||||
res = 7;
|
||||
goto out;
|
||||
}
|
||||
} else {
|
||||
*ret_p = seccomp_export_bpf_mem(ctx, NULL, &len);
|
||||
if (*ret_p != 0) {
|
||||
res = 6;
|
||||
goto out;
|
||||
}
|
||||
|
||||
buf = hakurei_scmp_allocate(allocate_p, len);
|
||||
if (buf == NULL) {
|
||||
res = 4;
|
||||
goto out;
|
||||
}
|
||||
|
||||
*ret_p = seccomp_export_bpf_mem(ctx, buf, &len);
|
||||
if (*ret_p != 0) {
|
||||
res = 6;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
if (ctx)
|
||||
seccomp_release(ctx);
|
||||
if (ctx)
|
||||
seccomp_release(ctx);
|
||||
|
||||
return res;
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
#include <seccomp.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#if (SCMP_VER_MAJOR < 2) || (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \
|
||||
#if (SCMP_VER_MAJOR < 2) || (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \
|
||||
(SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1)
|
||||
#error This package requires libseccomp >= v2.5.1
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
HAKUREI_EXPORT_MULTIARCH = 1 << 0,
|
||||
HAKUREI_EXPORT_CAN = 1 << 1,
|
||||
HAKUREI_EXPORT_BLUETOOTH = 1 << 2,
|
||||
HAKUREI_EXPORT_MULTIARCH = 1 << 0,
|
||||
HAKUREI_EXPORT_CAN = 1 << 1,
|
||||
HAKUREI_EXPORT_BLUETOOTH = 1 << 2,
|
||||
} hakurei_export_flag;
|
||||
|
||||
struct hakurei_syscall_rule {
|
||||
int syscall;
|
||||
int m_errno;
|
||||
struct scmp_arg_cmp *arg;
|
||||
int syscall;
|
||||
int m_errno;
|
||||
struct scmp_arg_cmp *arg;
|
||||
};
|
||||
|
||||
extern void *hakurei_scmp_allocate(uintptr_t f, size_t len);
|
||||
int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
|
||||
uint32_t arch, uint32_t multiarch,
|
||||
struct hakurei_syscall_rule *rules,
|
||||
size_t rules_sz, hakurei_export_flag flags);
|
||||
int32_t hakurei_scmp_make_filter(
|
||||
int *ret_p, uintptr_t allocate_p,
|
||||
uint32_t arch, uint32_t multiarch,
|
||||
struct hakurei_syscall_rule *rules,
|
||||
size_t rules_sz, hakurei_export_flag flags);
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"hakurei.app/container/stub"
|
||||
)
|
||||
|
||||
// Made available here to check panic recovery behaviour.
|
||||
//
|
||||
//go:linkname handleExitNew hakurei.app/container/stub.handleExitNew
|
||||
func handleExitNew(_ testing.TB)
|
||||
func handleExitNew(t testing.TB)
|
||||
|
||||
// overrideTFailNow overrides the Fail and FailNow method.
|
||||
type overrideTFailNow struct {
|
||||
|
||||
8
dist/release.sh
vendored
8
dist/release.sh
vendored
@@ -9,10 +9,10 @@ cp -v "README.md" "dist/hsurc.default" "dist/install.sh" "${out}"
|
||||
cp -rv "dist/comp" "${out}"
|
||||
|
||||
go generate ./...
|
||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
|
||||
-X hakurei.app/internal.buildVersion=${VERSION}
|
||||
-X hakurei.app/internal.hakureiPath=/usr/bin/hakurei
|
||||
-X hakurei.app/internal.hsuPath=/usr/bin/hsu
|
||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid=''
|
||||
-X hakurei.app/internal/info.buildVersion=${VERSION}
|
||||
-X hakurei.app/internal/info.hakureiPath=/usr/bin/hakurei
|
||||
-X hakurei.app/internal/info.hsuPath=/usr/bin/hsu
|
||||
-X main.hakureiPath=/usr/bin/hakurei" ./...
|
||||
|
||||
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
|
||||
|
||||
10
flake.nix
10
flake.nix
@@ -110,11 +110,11 @@
|
||||
in
|
||||
{
|
||||
default = hakurei;
|
||||
hakurei = pkgs.pkgsStatic.callPackage ./package.nix {
|
||||
hakurei = pkgs.callPackage ./package.nix {
|
||||
inherit (pkgs)
|
||||
# passthru.buildInputs
|
||||
go
|
||||
gcc
|
||||
clang
|
||||
|
||||
# nativeBuildInputs
|
||||
pkg-config
|
||||
@@ -129,6 +129,10 @@
|
||||
zstd
|
||||
gnutar
|
||||
coreutils
|
||||
|
||||
# for check
|
||||
util-linux
|
||||
nettools
|
||||
;
|
||||
};
|
||||
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
|
||||
@@ -144,7 +148,7 @@
|
||||
&& chmod -R +w .
|
||||
|
||||
export HAKUREI_VERSION="v${hakurei.version}"
|
||||
./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out
|
||||
CC="clang -O3 -Werror" ./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out
|
||||
'';
|
||||
}
|
||||
);
|
||||
|
||||
73
helper/deprecated.go
Normal file
73
helper/deprecated.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Package helper exposes the internal/helper package.
|
||||
//
|
||||
// Deprecated: This package will be removed in 0.4.
|
||||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/internal/helper"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
//go:linkname WaitDelay hakurei.app/internal/helper.WaitDelay
|
||||
var WaitDelay time.Duration
|
||||
|
||||
const (
|
||||
// HakureiHelper is set to 1 when args fd is enabled and 0 otherwise.
|
||||
HakureiHelper = helper.HakureiHelper
|
||||
// HakureiStatus is set to 1 when stat fd is enabled and 0 otherwise.
|
||||
HakureiStatus = helper.HakureiStatus
|
||||
)
|
||||
|
||||
type Helper = helper.Helper
|
||||
|
||||
// NewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
||||
//
|
||||
//go:linkname NewCheckedArgs hakurei.app/internal/helper.NewCheckedArgs
|
||||
func NewCheckedArgs(args ...string) (wt io.WriterTo, err error)
|
||||
|
||||
// MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
|
||||
// If s contains a NUL byte this function panics instead of returning an error.
|
||||
//
|
||||
//go:linkname MustNewCheckedArgs hakurei.app/internal/helper.MustNewCheckedArgs
|
||||
func MustNewCheckedArgs(args ...string) io.WriterTo
|
||||
|
||||
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
|
||||
// Function argF returns an array of arguments passed directly to the child process.
|
||||
//
|
||||
//go:linkname NewDirect hakurei.app/internal/helper.NewDirect
|
||||
func NewDirect(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
wt io.WriterTo,
|
||||
stat bool,
|
||||
argF func(argsFd, statFd int) []string,
|
||||
cmdF func(cmd *exec.Cmd),
|
||||
extraFiles []*os.File,
|
||||
) Helper
|
||||
|
||||
// New initialises a Helper instance with wt as the null-terminated argument writer.
|
||||
//
|
||||
//go:linkname New hakurei.app/internal/helper.New
|
||||
func New(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
pathname *check.Absolute, name string,
|
||||
wt io.WriterTo,
|
||||
stat bool,
|
||||
argF func(argsFd, statFd int) []string,
|
||||
cmdF func(z *container.Container),
|
||||
extraFiles []*os.File,
|
||||
) Helper
|
||||
|
||||
// InternalHelperStub is an internal function but exported because it is cross-package;
|
||||
// it is part of the implementation of the helper stub.
|
||||
func InternalHelperStub() { helper.InternalHelperStub() }
|
||||
63
helper/proc/deprecated.go
Normal file
63
helper/proc/deprecated.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Deprecated: This package will be removed in 0.4.
|
||||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
"hakurei.app/internal/helper/proc"
|
||||
)
|
||||
|
||||
//go:linkname FulfillmentTimeout hakurei.app/internal/helper/proc.FulfillmentTimeout
|
||||
var FulfillmentTimeout time.Duration
|
||||
|
||||
// A File is an extra file with deferred initialisation.
|
||||
type File = proc.File
|
||||
|
||||
// ExtraFilesPre is a linked list storing addresses of [os.File].
|
||||
type ExtraFilesPre = proc.ExtraFilesPre
|
||||
|
||||
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
|
||||
//
|
||||
//go:linkname Fulfill hakurei.app/internal/helper/proc.Fulfill
|
||||
func Fulfill(ctx context.Context,
|
||||
v *[]*os.File, start func() error,
|
||||
files []File, extraFiles *ExtraFilesPre,
|
||||
) (err error)
|
||||
|
||||
// InitFile initialises f as part of the slice extraFiles points to,
|
||||
// and returns its final fd value.
|
||||
//
|
||||
//go:linkname InitFile hakurei.app/internal/helper/proc.InitFile
|
||||
func InitFile(f File, extraFiles *ExtraFilesPre) (fd uintptr)
|
||||
|
||||
// BaseFile implements the Init method of the File interface and provides indirect access to extra file state.
|
||||
type BaseFile = proc.BaseFile
|
||||
|
||||
//go:linkname ExtraFile hakurei.app/internal/helper/proc.ExtraFile
|
||||
func ExtraFile(cmd *exec.Cmd, f *os.File) (fd uintptr)
|
||||
|
||||
//go:linkname ExtraFileSlice hakurei.app/internal/helper/proc.ExtraFileSlice
|
||||
func ExtraFileSlice(extraFiles *[]*os.File, f *os.File) (fd uintptr)
|
||||
|
||||
// NewWriterTo returns a [File] that receives content from wt on fulfillment.
|
||||
//
|
||||
//go:linkname NewWriterTo hakurei.app/internal/helper/proc.NewWriterTo
|
||||
func NewWriterTo(wt io.WriterTo) File
|
||||
|
||||
// NewStat returns a [File] implementing the behaviour
|
||||
// of the receiving end of xdg-dbus-proxy stat fd.
|
||||
//
|
||||
//go:linkname NewStat hakurei.app/internal/helper/proc.NewStat
|
||||
func NewStat(s *io.Closer) File
|
||||
|
||||
var (
|
||||
//go:linkname ErrStatFault hakurei.app/internal/helper/proc.ErrStatFault
|
||||
ErrStatFault error
|
||||
//go:linkname ErrStatRead hakurei.app/internal/helper/proc.ErrStatRead
|
||||
ErrStatRead error
|
||||
)
|
||||
@@ -54,6 +54,11 @@ type Paths struct {
|
||||
|
||||
// Info holds basic system information collected from the implementation.
|
||||
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"`
|
||||
// User is the userid according to hsu.
|
||||
|
||||
@@ -6,11 +6,13 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
_ "unsafe"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
// Made available here to check time encoding behaviour of [hst.ID].
|
||||
//
|
||||
//go:linkname newInstanceID hakurei.app/hst.newInstanceID
|
||||
func newInstanceID(id *hst.ID, p uint64) error
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
)
|
||||
|
||||
const testFileName = "acl.test"
|
||||
90
internal/acl/libacl-helper.c
Normal file
90
internal/acl/libacl-helper.c
Normal file
@@ -0,0 +1,90 @@
|
||||
#include "libacl-helper.h"
|
||||
#include <acl/libacl.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/acl.h>
|
||||
|
||||
int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
|
||||
acl_perm_t *perms, size_t plen) {
|
||||
int ret;
|
||||
bool v;
|
||||
int i;
|
||||
acl_t acl;
|
||||
acl_entry_t entry;
|
||||
acl_tag_t tag_type;
|
||||
void *qualifier_p;
|
||||
acl_permset_t permset;
|
||||
|
||||
ret = -1; /* acl_get_file */
|
||||
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
|
||||
if (acl == NULL)
|
||||
goto out;
|
||||
|
||||
/* prune entries by uid */
|
||||
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
|
||||
i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
|
||||
ret = -2; /* acl_get_tag_type */
|
||||
if (acl_get_tag_type(entry, &tag_type) != 0)
|
||||
goto out;
|
||||
if (tag_type != ACL_USER)
|
||||
continue;
|
||||
|
||||
ret = -3; /* acl_get_qualifier */
|
||||
qualifier_p = acl_get_qualifier(entry);
|
||||
if (qualifier_p == NULL)
|
||||
goto out;
|
||||
v = *(uid_t *)qualifier_p == uid;
|
||||
acl_free(qualifier_p);
|
||||
|
||||
if (!v)
|
||||
continue;
|
||||
|
||||
ret = -4; /* acl_delete_entry */
|
||||
if (acl_delete_entry(acl, entry) != 0)
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (plen == 0)
|
||||
goto set;
|
||||
|
||||
ret = -5; /* acl_create_entry */
|
||||
if (acl_create_entry(&acl, &entry) != 0)
|
||||
goto out;
|
||||
|
||||
ret = -6; /* acl_get_permset */
|
||||
if (acl_get_permset(entry, &permset) != 0)
|
||||
goto out;
|
||||
|
||||
ret = -7; /* acl_add_perm */
|
||||
for (i = 0; i < plen; i++) {
|
||||
if (acl_add_perm(permset, perms[i]) != 0)
|
||||
goto out;
|
||||
}
|
||||
|
||||
ret = -8; /* acl_set_tag_type */
|
||||
if (acl_set_tag_type(entry, ACL_USER) != 0)
|
||||
goto out;
|
||||
|
||||
ret = -9; /* acl_set_qualifier */
|
||||
if (acl_set_qualifier(entry, (void *)&uid) != 0)
|
||||
goto out;
|
||||
|
||||
set:
|
||||
ret = -10; /* acl_calc_mask */
|
||||
if (acl_calc_mask(&acl) != 0)
|
||||
goto out;
|
||||
|
||||
ret = -11; /* acl_valid */
|
||||
if (acl_valid(acl) != 0)
|
||||
goto out;
|
||||
|
||||
ret = -12; /* acl_set_file */
|
||||
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
|
||||
ret = 0;
|
||||
|
||||
out:
|
||||
free((void *)path_p);
|
||||
if (acl != NULL)
|
||||
acl_free((void *)acl);
|
||||
return ret;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package acl_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
)
|
||||
|
||||
func TestPerms(t *testing.T) {
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/internal/dbus"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/internal/dbus"
|
||||
)
|
||||
|
||||
func TestConfigArgs(t *testing.T) {
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/helper"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func TestFinalise(t *testing.T) {
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/internal/helper"
|
||||
"hakurei.app/ldd"
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ func (p *Proxy) Start() error {
|
||||
}
|
||||
|
||||
var libPaths []*check.Absolute
|
||||
if entries, err := ldd.Exec(ctx, p.msg, toolPath.String()); err != nil {
|
||||
if entries, err := ldd.Resolve(ctx, p.msg, toolPath); err != nil {
|
||||
return err
|
||||
} else {
|
||||
libPaths = ldd.Path(entries)
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/internal/helper"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) }
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/helper"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/internal/helper"
|
||||
)
|
||||
|
||||
func TestArgsString(t *testing.T) {
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/helper/proc"
|
||||
"hakurei.app/internal/helper/proc"
|
||||
)
|
||||
|
||||
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/internal/helper"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/helper/proc"
|
||||
"hakurei.app/internal/helper/proc"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/internal/helper"
|
||||
)
|
||||
|
||||
func TestContainer(t *testing.T) {
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"hakurei.app/helper/proc"
|
||||
"hakurei.app/internal/helper/proc"
|
||||
)
|
||||
|
||||
var WaitDelay = 2 * time.Second
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/internal/helper"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/internal/helper"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) }
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package info
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package info
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package info
|
||||
|
||||
// FallbackVersion is returned when a version string was not set by the linker.
|
||||
const FallbackVersion = "dirty"
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
// osFile represents [os.File].
|
||||
@@ -156,7 +156,7 @@ func (direct) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) erro
|
||||
return seccomp.Load(rules, flags)
|
||||
}
|
||||
|
||||
func (direct) mustHsuPath() *check.Absolute { return internal.MustHsuPath() }
|
||||
func (direct) mustHsuPath() *check.Absolute { return info.MustHsuPath() }
|
||||
|
||||
func (direct) dbusAddress() (session, system string) { return dbus.Address() }
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
// call initialises a [stub.Call].
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"os/user"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
func newWithMessage(msg string) error { return newWithMessageError(msg, os.ErrInvalid) }
|
||||
|
||||
@@ -9,12 +9,25 @@ import (
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"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"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
// Info returns the address to a populated [hst.Info].
|
||||
//
|
||||
// This must not be called from within package outcome.
|
||||
func Info() *hst.Info {
|
||||
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
|
||||
}
|
||||
|
||||
// envAllocSize is the initial size of the env map pre-allocated when the configured env map is nil.
|
||||
// It should be large enough to fit all insertions by outcomeOp.toContainer.
|
||||
const envAllocSize = 1 << 6
|
||||
|
||||
@@ -16,10 +16,10 @@ import (
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/store"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,7 +39,7 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
|
||||
}
|
||||
|
||||
// read comp value early for early failure
|
||||
hsuPath := internal.MustHsuPath()
|
||||
hsuPath := info.MustHsuPath()
|
||||
|
||||
const (
|
||||
// transitions to processCommit, or processFinal on failure
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
|
||||
// IsPollDescriptor reports whether fd is the descriptor being used by the poller.
|
||||
//
|
||||
// Made available here to determine and reject impossible fd.
|
||||
//
|
||||
//go:linkname IsPollDescriptor internal/poll.IsPollDescriptor
|
||||
func IsPollDescriptor(fd uintptr) bool
|
||||
|
||||
@@ -21,10 +21,10 @@ import (
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func TestOutcomeMain(t *testing.T) {
|
||||
@@ -141,7 +141,7 @@ func TestOutcomeMain(t *testing.T) {
|
||||
Proc(fhs.AbsProc).
|
||||
Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
|
||||
Bind(fhs.AbsDev, fhs.AbsDev, std.BindWritable|std.BindDevice).
|
||||
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777).
|
||||
Tmpfs(fhs.AbsDevShm, 0, 01777).
|
||||
|
||||
// spRuntimeOp
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
@@ -243,7 +243,7 @@ func TestOutcomeMain(t *testing.T) {
|
||||
Proc(m("/proc/")).
|
||||
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
|
||||
DevWritable(m("/dev/"), true).
|
||||
Tmpfs(m("/dev/shm"), 0, 01777).
|
||||
Tmpfs(m("/dev/shm/"), 0, 01777).
|
||||
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/0"), m("/run/user/65534"), std.BindWritable).
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), std.BindWritable).
|
||||
@@ -412,7 +412,7 @@ func TestOutcomeMain(t *testing.T) {
|
||||
Proc(m("/proc/")).
|
||||
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
|
||||
DevWritable(m("/dev/"), true).
|
||||
Tmpfs(m("/dev/shm"), 0, 01777).
|
||||
Tmpfs(m("/dev/shm/"), 0, 01777).
|
||||
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/65534"), std.BindWritable).
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/9"), m("/tmp/"), std.BindWritable).
|
||||
@@ -558,7 +558,7 @@ func TestOutcomeMain(t *testing.T) {
|
||||
Proc(m("/proc/")).
|
||||
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
|
||||
DevWritable(m("/dev/"), true).
|
||||
Tmpfs(m("/dev/shm"), 0, 01777).
|
||||
Tmpfs(m("/dev/shm/"), 0, 01777).
|
||||
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), std.BindWritable).
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), std.BindWritable).
|
||||
@@ -10,53 +10,53 @@ static int hakurei_shim_fd = -1;
|
||||
|
||||
/* see shim.go for handling of the message */
|
||||
static inline ssize_t hakurei_shim_write(hakurei_shim_msg msg) {
|
||||
int savedErrno = errno;
|
||||
unsigned char buf = (unsigned char)msg;
|
||||
ssize_t ret = write(hakurei_shim_fd, &buf, 1);
|
||||
if (ret == -1 && errno != EAGAIN)
|
||||
exit(EXIT_FAILURE);
|
||||
errno = savedErrno;
|
||||
return ret;
|
||||
int savedErrno = errno;
|
||||
unsigned char buf = (unsigned char)msg;
|
||||
ssize_t ret = write(hakurei_shim_fd, &buf, 1);
|
||||
if (ret == -1 && errno != EAGAIN)
|
||||
exit(EXIT_FAILURE);
|
||||
errno = savedErrno;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
|
||||
if (sig != SIGCONT || si == NULL) {
|
||||
hakurei_shim_write(HAKUREI_SHIM_INVALID);
|
||||
return;
|
||||
}
|
||||
if (sig != SIGCONT || si == NULL) {
|
||||
hakurei_shim_write(HAKUREI_SHIM_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (si->si_pid == hakurei_shim_param_ppid) {
|
||||
hakurei_shim_write(HAKUREI_SHIM_EXIT_REQUESTED);
|
||||
return;
|
||||
}
|
||||
if (si->si_pid == hakurei_shim_param_ppid) {
|
||||
hakurei_shim_write(HAKUREI_SHIM_EXIT_REQUESTED);
|
||||
return;
|
||||
}
|
||||
|
||||
hakurei_shim_write(HAKUREI_SHIM_BAD_PID);
|
||||
hakurei_shim_write(HAKUREI_SHIM_BAD_PID);
|
||||
|
||||
if (getppid() != hakurei_shim_param_ppid)
|
||||
hakurei_shim_write(HAKUREI_SHIM_ORPHAN);
|
||||
if (getppid() != hakurei_shim_param_ppid)
|
||||
hakurei_shim_write(HAKUREI_SHIM_ORPHAN);
|
||||
}
|
||||
|
||||
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd) {
|
||||
if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1)
|
||||
*(int *)NULL = 0; /* unreachable */
|
||||
if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1)
|
||||
*(volatile int *)NULL = 0; /* unreachable */
|
||||
|
||||
struct sigaction new_action = {0}, old_action = {0};
|
||||
if (sigaction(SIGCONT, NULL, &old_action) != 0)
|
||||
return;
|
||||
if (old_action.sa_handler != SIG_DFL) {
|
||||
errno = ENOTRECOVERABLE;
|
||||
return;
|
||||
}
|
||||
struct sigaction new_action = {0}, old_action = {0};
|
||||
if (sigaction(SIGCONT, NULL, &old_action) != 0)
|
||||
return;
|
||||
if (old_action.sa_handler != SIG_DFL) {
|
||||
errno = ENOTRECOVERABLE;
|
||||
return;
|
||||
}
|
||||
|
||||
new_action.sa_sigaction = hakurei_shim_sigaction;
|
||||
if (sigemptyset(&new_action.sa_mask) != 0)
|
||||
return;
|
||||
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
|
||||
new_action.sa_sigaction = hakurei_shim_sigaction;
|
||||
if (sigemptyset(&new_action.sa_mask) != 0)
|
||||
return;
|
||||
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
|
||||
|
||||
if (sigaction(SIGCONT, &new_action, NULL) != 0)
|
||||
return;
|
||||
if (sigaction(SIGCONT, &new_action, NULL) != 0)
|
||||
return;
|
||||
|
||||
errno = 0;
|
||||
hakurei_shim_param_ppid = ppid;
|
||||
hakurei_shim_fd = fd;
|
||||
errno = 0;
|
||||
hakurei_shim_param_ppid = ppid;
|
||||
hakurei_shim_fd = fd;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
/* see shim.go for documentation */
|
||||
typedef enum {
|
||||
HAKUREI_SHIM_EXIT_REQUESTED,
|
||||
HAKUREI_SHIM_ORPHAN,
|
||||
HAKUREI_SHIM_INVALID,
|
||||
HAKUREI_SHIM_BAD_PID,
|
||||
HAKUREI_SHIM_EXIT_REQUESTED,
|
||||
HAKUREI_SHIM_ORPHAN,
|
||||
HAKUREI_SHIM_INVALID,
|
||||
HAKUREI_SHIM_BAD_PID,
|
||||
} hakurei_shim_msg;
|
||||
|
||||
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd);
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestShimEntrypoint(t *testing.T) {
|
||||
Proc(fhs.AbsProc).
|
||||
Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
|
||||
Bind(fhs.AbsDev, fhs.AbsDev, std.BindWritable|std.BindDevice).
|
||||
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777).
|
||||
Tmpfs(fhs.AbsDevShm, 0, 01777).
|
||||
|
||||
// spRuntimeOp
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
|
||||
@@ -16,11 +16,11 @@ import (
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/validate"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
const varRunNscd = fhs.Var + "run/nscd"
|
||||
@@ -116,7 +116,7 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
|
||||
state.params.Bind(fhs.AbsDev, fhs.AbsDev, std.BindWritable|std.BindDevice)
|
||||
}
|
||||
// /dev is mounted readonly later on, this prevents /dev/shm from going readonly with it
|
||||
state.params.Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777)
|
||||
state.params.Tmpfs(fhs.AbsDevShm, 0, 01777)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/system"
|
||||
)
|
||||
|
||||
func TestSpParamsOp(t *testing.T) {
|
||||
@@ -72,7 +72,7 @@ func TestSpParamsOp(t *testing.T) {
|
||||
Root(m("/var/lib/hakurei/base/org.debian"), std.BindWritable).
|
||||
Proc(fhs.AbsProc).Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
|
||||
DevWritable(fhs.AbsDev, true).
|
||||
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777),
|
||||
Tmpfs(fhs.AbsDevShm, 0, 01777),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"TERM": "xterm",
|
||||
}, func(t *testing.T, state *outcomeStateParams) {
|
||||
@@ -110,7 +110,7 @@ func TestSpParamsOp(t *testing.T) {
|
||||
Root(m("/var/lib/hakurei/base/org.debian"), std.BindWritable).
|
||||
Proc(fhs.AbsProc).Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
|
||||
Bind(fhs.AbsDev, fhs.AbsDev, std.BindWritable|std.BindDevice).
|
||||
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777),
|
||||
Tmpfs(fhs.AbsDevShm, 0, 01777),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"TERM": "xterm",
|
||||
}, func(t *testing.T, state *outcomeStateParams) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/dbus"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(spDBusOp)) }
|
||||
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/helper"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func TestSpDBusOp(t *testing.T) {
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/system"
|
||||
)
|
||||
|
||||
func TestSpPulseOp(t *testing.T) {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/system"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/system"
|
||||
)
|
||||
|
||||
func TestSpRuntimeOp(t *testing.T) {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/system"
|
||||
)
|
||||
|
||||
func init() { gob.Register(spTmpdirOp{}) }
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/system"
|
||||
)
|
||||
|
||||
func TestSpTmpdirOp(t *testing.T) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/wayland"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/wayland"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(spWaylandOp)) }
|
||||
@@ -25,8 +25,8 @@ func (s *spWaylandOp) toSystem(state *outcomeStateSys) error {
|
||||
|
||||
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
|
||||
var socketPath *check.Absolute
|
||||
if name, ok := state.k.lookupEnv(wayland.WaylandDisplay); !ok {
|
||||
state.msg.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
|
||||
if name, ok := state.k.lookupEnv(wayland.Display); !ok {
|
||||
state.msg.Verbose(wayland.Display + " is not set, assuming " + wayland.FallbackName)
|
||||
socketPath = state.sc.RuntimePath.Append(wayland.FallbackName)
|
||||
} else if a, err := check.NewAbs(name); err != nil {
|
||||
socketPath = state.sc.RuntimePath.Append(name)
|
||||
@@ -53,7 +53,7 @@ func (s *spWaylandOp) toSystem(state *outcomeStateSys) error {
|
||||
|
||||
func (s *spWaylandOp) toContainer(state *outcomeStateParams) error {
|
||||
innerPath := state.runtimeDir.Append(wayland.FallbackName)
|
||||
state.env[wayland.WaylandDisplay] = wayland.FallbackName
|
||||
state.env[wayland.Display] = wayland.FallbackName
|
||||
if s.SocketPath == nil {
|
||||
state.params.Bind(state.instancePath().Append("wayland"), innerPath, 0)
|
||||
} else {
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/wayland"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/system"
|
||||
"hakurei.app/internal/wayland"
|
||||
)
|
||||
|
||||
func TestSpWaylandOp(t *testing.T) {
|
||||
@@ -47,7 +47,7 @@ func TestSpWaylandOp(t *testing.T) {
|
||||
Ops: new(container.Ops).
|
||||
Bind(m(wantInstancePrefix+"/wayland"), m("/run/user/1000/wayland-0"), 0),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
wayland.WaylandDisplay: wayland.FallbackName,
|
||||
wayland.Display: wayland.FallbackName,
|
||||
}, nil), nil},
|
||||
|
||||
{"success direct", func(isShim, _ bool) outcomeOp {
|
||||
@@ -75,7 +75,7 @@ func TestSpWaylandOp(t *testing.T) {
|
||||
Ops: new(container.Ops).
|
||||
Bind(m("/proc/nonexistent/wayland"), m("/run/user/1000/wayland-0"), 0),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
wayland.WaylandDisplay: wayland.FallbackName,
|
||||
wayland.Display: wayland.FallbackName,
|
||||
}, nil), nil},
|
||||
|
||||
{"success", func(bool, bool) outcomeOp {
|
||||
@@ -98,7 +98,7 @@ func TestSpWaylandOp(t *testing.T) {
|
||||
Ops: new(container.Ops).
|
||||
Bind(m(wantInstancePrefix+"/wayland"), m("/run/user/1000/wayland-0"), 0),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
wayland.WaylandDisplay: wayland.FallbackName,
|
||||
wayland.Display: wayland.FallbackName,
|
||||
}, nil), nil},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
)
|
||||
|
||||
var absX11SocketDir = fhs.AbsTmp.Append(".X11-unix")
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
)
|
||||
|
||||
func TestSpX11Op(t *testing.T) {
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func TestEntryData(t *testing.T) {
|
||||
return buf.String()
|
||||
}
|
||||
}
|
||||
templateStateGob := mustEncodeGob(newTemplateState())
|
||||
templateStateGob := mustEncodeGob(NewTemplateState())
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -45,11 +45,11 @@ func TestEntryData(t *testing.T) {
|
||||
Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "invalid configuration"}},
|
||||
|
||||
{"inconsistent enablement", "\x00\xff\xca\xfe\x00\x00\xff\x00" + templateStateGob, newTemplateState(), &hst.AppError{
|
||||
{"inconsistent enablement", "\x00\xff\xca\xfe\x00\x00\xff\x00" + templateStateGob, NewTemplateState(), &hst.AppError{
|
||||
Step: "validate state enablement", Err: os.ErrInvalid,
|
||||
Msg: "state entry aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa has unexpected enablement byte 0xd, 0xff"}},
|
||||
|
||||
{"template", "\x00\xff\xca\xfe\x00\x00\x0d\xf2" + templateStateGob, newTemplateState(), nil},
|
||||
{"template", "\x00\xff\xca\xfe\x00\x00\x0d\xf2" + templateStateGob, NewTemplateState(), nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -105,7 +105,7 @@ func TestEntryData(t *testing.T) {
|
||||
|
||||
t.Run("encode fault", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTemplateState()
|
||||
s := NewTemplateState()
|
||||
|
||||
t.Run("gob", func(t *testing.T) {
|
||||
var want = &hst.AppError{Step: "encode state body", Err: stub.UniqueError(0xcafe)}
|
||||
@@ -123,8 +123,8 @@ func TestEntryData(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// newTemplateState returns the address of a new template [hst.State] struct.
|
||||
func newTemplateState() *hst.State {
|
||||
// NewTemplateState returns the address of a new template [hst.State] struct.
|
||||
func NewTemplateState() *hst.State {
|
||||
return &hst.State{
|
||||
ID: hst.ID(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))),
|
||||
PID: 0xcafe,
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
_ "unsafe"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/stub"
|
||||
@@ -18,18 +18,23 @@ import (
|
||||
"hakurei.app/internal/store"
|
||||
)
|
||||
|
||||
//go:linkname newTemplateState hakurei.app/internal/store.newTemplateState
|
||||
func newTemplateState() *hst.State
|
||||
|
||||
// Made available here for direct validation of state entry files.
|
||||
//
|
||||
//go:linkname entryDecode hakurei.app/internal/store.entryDecode
|
||||
func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error)
|
||||
|
||||
// Made available here for direct access to known segment handles.
|
||||
//
|
||||
//go:linkname newHandle hakurei.app/internal/store.newHandle
|
||||
func newHandle(base *check.Absolute, identity int) *store.Handle
|
||||
|
||||
// Made available here to check open error handling behaviour.
|
||||
//
|
||||
//go:linkname open hakurei.app/internal/store.(*EntryHandle).open
|
||||
func open(eh *store.EntryHandle, flag int, perm os.FileMode) (*os.File, error)
|
||||
|
||||
// Made available here to check the saveload cycle.
|
||||
//
|
||||
//go:linkname save hakurei.app/internal/store.(*EntryHandle).save
|
||||
func save(eh *store.EntryHandle, state *hst.State) error
|
||||
|
||||
@@ -91,9 +96,9 @@ func TestStateEntryHandle(t *testing.T) {
|
||||
t.Run("saveload", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
eh := store.EntryHandle{Pathname: check.MustAbs(t.TempDir()).Append("entry"),
|
||||
ID: newTemplateState().ID}
|
||||
ID: store.NewTemplateState().ID}
|
||||
|
||||
if err := save(&eh, newTemplateState()); err != nil {
|
||||
if err := save(&eh, store.NewTemplateState()); err != nil {
|
||||
t.Fatalf("save: error = %v", err)
|
||||
}
|
||||
|
||||
@@ -112,7 +117,7 @@ func TestStateEntryHandle(t *testing.T) {
|
||||
t.Fatal(f.Close())
|
||||
}
|
||||
|
||||
if want := newTemplateState(); !reflect.DeepEqual(&got, want) {
|
||||
if want := store.NewTemplateState(); !reflect.DeepEqual(&got, want) {
|
||||
t.Errorf("entryDecode: %#v, want %#v", &got, want)
|
||||
}
|
||||
})
|
||||
@@ -122,7 +127,7 @@ func TestStateEntryHandle(t *testing.T) {
|
||||
|
||||
if et, err := eh.Load(nil); err != nil {
|
||||
t.Fatalf("load: error = %v", err)
|
||||
} else if want := newTemplateState().Enablements.Unwrap(); et != want {
|
||||
} else if want := store.NewTemplateState().Enablements.Unwrap(); et != want {
|
||||
t.Errorf("load: et = %x, want %x", et, want)
|
||||
}
|
||||
})
|
||||
@@ -133,7 +138,7 @@ func TestStateEntryHandle(t *testing.T) {
|
||||
var got hst.State
|
||||
if _, err := eh.Load(&got); err != nil {
|
||||
t.Fatalf("load: error = %v", err)
|
||||
} else if want := newTemplateState(); !reflect.DeepEqual(&got, want) {
|
||||
} else if want := store.NewTemplateState(); !reflect.DeepEqual(&got, want) {
|
||||
t.Errorf("load: %#v, want %#v", &got, want)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,13 +12,15 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
_ "unsafe"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/store"
|
||||
)
|
||||
|
||||
// Made available here to check bigLock error handling behaviour.
|
||||
//
|
||||
//go:linkname bigLock hakurei.app/internal/store.(*Store).bigLock
|
||||
func bigLock(s *store.Store) (unlock func(), err error)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
)
|
||||
|
||||
// UpdatePerm calls UpdatePermType with the [Process] criteria.
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/internal/acl"
|
||||
)
|
||||
|
||||
func TestACLUpdateOp(t *testing.T) {
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/internal/dbus"
|
||||
)
|
||||
|
||||
// ErrDBusConfig is returned when a required [hst.BusConfig] argument is nil.
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/helper"
|
||||
)
|
||||
|
||||
func TestDBusProxyOp(t *testing.T) {
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/system/internal/xcb"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/wayland"
|
||||
"hakurei.app/internal/xcb"
|
||||
)
|
||||
|
||||
type osFile interface {
|
||||
@@ -45,6 +47,8 @@ type syscallDispatcher interface {
|
||||
// aclUpdate provides [acl.Update].
|
||||
aclUpdate(name string, uid int, perms ...acl.Perm) error
|
||||
|
||||
waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (*wayland.SecurityContext, error)
|
||||
|
||||
// xcbChangeHosts provides [xcb.ChangeHosts].
|
||||
xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error
|
||||
|
||||
@@ -76,6 +80,10 @@ func (k direct) aclUpdate(name string, uid int, perms ...acl.Perm) error {
|
||||
return acl.Update(name, uid, perms...)
|
||||
}
|
||||
|
||||
func (k direct) waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (*wayland.SecurityContext, error) {
|
||||
return wayland.New(displayPath, bindPath, appID, instanceID)
|
||||
}
|
||||
|
||||
func (k direct) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error {
|
||||
return xcb.ChangeHosts(mode, family, address)
|
||||
}
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/system/internal/xcb"
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/wayland"
|
||||
"hakurei.app/internal/xcb"
|
||||
)
|
||||
|
||||
// call initialises a [stub.Call].
|
||||
@@ -268,6 +270,15 @@ func (k *kstub) aclUpdate(name string, uid int, perms ...acl.Perm) error {
|
||||
stub.CheckArgReflect(k.Stub, "perms", perms, 2))
|
||||
}
|
||||
|
||||
func (k *kstub) waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (*wayland.SecurityContext, error) {
|
||||
k.Helper()
|
||||
return nil, k.Expects("waylandNew").Error(
|
||||
stub.CheckArgReflect(k.Stub, "displayPath", displayPath, 0),
|
||||
stub.CheckArgReflect(k.Stub, "bindPath", bindPath, 1),
|
||||
stub.CheckArg(k.Stub, "appID", appID, 2),
|
||||
stub.CheckArg(k.Stub, "instanceID", instanceID, 3))
|
||||
}
|
||||
|
||||
func (k *kstub) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error {
|
||||
k.Helper()
|
||||
return k.Expects("xcbChangeHosts").Error(
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user