From a91920310d20e1fc472dea044fce8fd9438835df Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sat, 15 Nov 2025 13:47:09 +0900 Subject: internal: relocate packages Signed-off-by: Ophestra --- cmd/hakurei/command.go | 2 +- internal/acl/acl.go | 33 ++ internal/acl/acl_test.go | 276 +++++++++++++++ internal/acl/libacl-helper.c | 90 +++++ internal/acl/libacl-helper.go | 40 +++ internal/acl/libacl-helper.h | 4 + internal/acl/libacl-helper_test.go | 60 ++++ internal/acl/perms.go | 18 + internal/acl/perms_test.go | 30 ++ internal/dbus/address.go | 193 ++++++++++ internal/dbus/address_escape_test.go | 59 ++++ internal/dbus/address_test.go | 123 +++++++ internal/dbus/config.go | 71 ++++ internal/dbus/config_test.go | 124 +++++++ internal/dbus/dbus.go | 69 ++++ internal/dbus/dbus_test.go | 170 +++++++++ internal/dbus/export_test.go | 15 + internal/dbus/proc.go | 188 ++++++++++ internal/dbus/proc_test.go | 11 + internal/dbus/proxy.go | 105 ++++++ internal/dbus/samples_test.go | 214 +++++++++++ internal/dbus/testdata/dev.vencord.Vesktop.json | 18 + internal/dbus/testdata/org.chromium.Chromium+.json | 9 + internal/dbus/testdata/org.chromium.Chromium.json | 24 ++ .../dbus/testdata/uk.gensokyo.CrashTestDummy.json | 21 ++ internal/outcome/dispatcher.go | 2 +- internal/outcome/outcome.go | 2 +- internal/outcome/run_test.go | 4 +- internal/outcome/spcontainer.go | 4 +- internal/outcome/spcontainer_test.go | 4 +- internal/outcome/spdbus.go | 4 +- internal/outcome/spdbus_test.go | 4 +- internal/outcome/sppulse_test.go | 2 +- internal/outcome/spruntime.go | 2 +- internal/outcome/spruntime_test.go | 2 +- internal/outcome/sptmpdir.go | 2 +- internal/outcome/sptmpdir_test.go | 2 +- internal/outcome/spwayland.go | 4 +- internal/outcome/spwayland_test.go | 4 +- internal/outcome/spx11.go | 2 +- internal/outcome/spx11_test.go | 2 +- internal/system/acl.go | 2 +- internal/system/acl/acl.go | 33 -- internal/system/acl/acl_test.go | 276 --------------- internal/system/acl/libacl-helper.c | 90 ----- internal/system/acl/libacl-helper.go | 40 --- internal/system/acl/libacl-helper.h | 4 - internal/system/acl/libacl-helper_test.go | 60 ---- internal/system/acl/perms.go | 18 - internal/system/acl/perms_test.go | 30 -- internal/system/acl_test.go | 2 +- internal/system/dbus.go | 2 +- internal/system/dbus/address.go | 193 ---------- internal/system/dbus/address_escape_test.go | 59 ---- internal/system/dbus/address_test.go | 123 ------- internal/system/dbus/config.go | 71 ---- internal/system/dbus/config_test.go | 124 ------- internal/system/dbus/dbus.go | 69 ---- internal/system/dbus/dbus_test.go | 170 --------- internal/system/dbus/export_test.go | 15 - internal/system/dbus/proc.go | 188 ---------- internal/system/dbus/proc_test.go | 11 - internal/system/dbus/proxy.go | 105 ------ internal/system/dbus/samples_test.go | 214 ----------- .../system/dbus/testdata/dev.vencord.Vesktop.json | 18 - .../dbus/testdata/org.chromium.Chromium+.json | 9 - .../dbus/testdata/org.chromium.Chromium.json | 24 -- .../dbus/testdata/uk.gensokyo.CrashTestDummy.json | 21 -- internal/system/dbus_test.go | 2 +- internal/system/dispatcher.go | 6 +- internal/system/dispatcher_test.go | 6 +- internal/system/system_test.go | 2 +- internal/system/wayland.go | 4 +- internal/system/wayland/conn.go | 122 ------- .../system/wayland/security-context-v1-protocol.c | 74 ---- .../system/wayland/security-context-v1-protocol.h | 392 --------------------- internal/system/wayland/wayland-client-helper.c | 96 ----- internal/system/wayland/wayland-client-helper.h | 4 - internal/system/wayland/wayland.go | 48 --- internal/system/wayland_test.go | 4 +- internal/system/xcb/changehosts.go | 17 - internal/system/xcb/xcb.go | 127 ------- internal/system/xhost.go | 2 +- internal/system/xhost_test.go | 2 +- internal/wayland/conn.go | 122 +++++++ internal/wayland/security-context-v1-protocol.c | 74 ++++ internal/wayland/security-context-v1-protocol.h | 392 +++++++++++++++++++++ internal/wayland/wayland-client-helper.c | 96 +++++ internal/wayland/wayland-client-helper.h | 4 + internal/wayland/wayland.go | 48 +++ internal/xcb/changehosts.go | 17 + internal/xcb/xcb.go | 127 +++++++ system/acl/deprecated.go | 6 +- system/dbus/deprecated.go | 20 +- system/wayland/deprecated.go | 4 +- 95 files changed, 2901 insertions(+), 2901 deletions(-) create mode 100644 internal/acl/acl.go create mode 100644 internal/acl/acl_test.go create mode 100644 internal/acl/libacl-helper.c create mode 100644 internal/acl/libacl-helper.go create mode 100644 internal/acl/libacl-helper.h create mode 100644 internal/acl/libacl-helper_test.go create mode 100644 internal/acl/perms.go create mode 100644 internal/acl/perms_test.go create mode 100644 internal/dbus/address.go create mode 100644 internal/dbus/address_escape_test.go create mode 100644 internal/dbus/address_test.go create mode 100644 internal/dbus/config.go create mode 100644 internal/dbus/config_test.go create mode 100644 internal/dbus/dbus.go create mode 100644 internal/dbus/dbus_test.go create mode 100644 internal/dbus/export_test.go create mode 100644 internal/dbus/proc.go create mode 100644 internal/dbus/proc_test.go create mode 100644 internal/dbus/proxy.go create mode 100644 internal/dbus/samples_test.go create mode 100644 internal/dbus/testdata/dev.vencord.Vesktop.json create mode 100644 internal/dbus/testdata/org.chromium.Chromium+.json create mode 100644 internal/dbus/testdata/org.chromium.Chromium.json create mode 100644 internal/dbus/testdata/uk.gensokyo.CrashTestDummy.json delete mode 100644 internal/system/acl/acl.go delete mode 100644 internal/system/acl/acl_test.go delete mode 100644 internal/system/acl/libacl-helper.c delete mode 100644 internal/system/acl/libacl-helper.go delete mode 100644 internal/system/acl/libacl-helper.h delete mode 100644 internal/system/acl/libacl-helper_test.go delete mode 100644 internal/system/acl/perms.go delete mode 100644 internal/system/acl/perms_test.go delete mode 100644 internal/system/dbus/address.go delete mode 100644 internal/system/dbus/address_escape_test.go delete mode 100644 internal/system/dbus/address_test.go delete mode 100644 internal/system/dbus/config.go delete mode 100644 internal/system/dbus/config_test.go delete mode 100644 internal/system/dbus/dbus.go delete mode 100644 internal/system/dbus/dbus_test.go delete mode 100644 internal/system/dbus/export_test.go delete mode 100644 internal/system/dbus/proc.go delete mode 100644 internal/system/dbus/proc_test.go delete mode 100644 internal/system/dbus/proxy.go delete mode 100644 internal/system/dbus/samples_test.go delete mode 100644 internal/system/dbus/testdata/dev.vencord.Vesktop.json delete mode 100644 internal/system/dbus/testdata/org.chromium.Chromium+.json delete mode 100644 internal/system/dbus/testdata/org.chromium.Chromium.json delete mode 100644 internal/system/dbus/testdata/uk.gensokyo.CrashTestDummy.json delete mode 100644 internal/system/wayland/conn.go delete mode 100644 internal/system/wayland/security-context-v1-protocol.c delete mode 100644 internal/system/wayland/security-context-v1-protocol.h delete mode 100644 internal/system/wayland/wayland-client-helper.c delete mode 100644 internal/system/wayland/wayland-client-helper.h delete mode 100644 internal/system/wayland/wayland.go delete mode 100644 internal/system/xcb/changehosts.go delete mode 100644 internal/system/xcb/xcb.go create mode 100644 internal/wayland/conn.go create mode 100644 internal/wayland/security-context-v1-protocol.c create mode 100644 internal/wayland/security-context-v1-protocol.h create mode 100644 internal/wayland/wayland-client-helper.c create mode 100644 internal/wayland/wayland-client-helper.h create mode 100644 internal/wayland/wayland.go create mode 100644 internal/xcb/changehosts.go create mode 100644 internal/xcb/xcb.go diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index 2b0568e2..5d33d41b 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -17,10 +17,10 @@ import ( "hakurei.app/container/check" "hakurei.app/container/fhs" "hakurei.app/hst" + "hakurei.app/internal/dbus" "hakurei.app/internal/env" "hakurei.app/internal/info" "hakurei.app/internal/outcome" - "hakurei.app/internal/system/dbus" "hakurei.app/message" ) diff --git a/internal/acl/acl.go b/internal/acl/acl.go new file mode 100644 index 00000000..366ad533 --- /dev/null +++ b/internal/acl/acl.go @@ -0,0 +1,33 @@ +// Package acl implements simple ACL manipulation via libacl. +package acl + +/* +#cgo linux pkg-config: --static libacl + +#include "libacl-helper.h" +*/ +import "C" + +type Perm C.acl_perm_t + +const ( + Read Perm = C.ACL_READ + Write Perm = C.ACL_WRITE + Execute Perm = C.ACL_EXECUTE +) + +// Update replaces ACL_USER entry with qualifier uid. +func Update(name string, uid int, perms ...Perm) error { + var p *Perm + if len(perms) > 0 { + p = &perms[0] + } + + r, err := C.hakurei_acl_update_file_by_uid( + C.CString(name), + C.uid_t(uid), + (*C.acl_perm_t)(p), + C.size_t(len(perms)), + ) + return newAclPathError(name, int(r), err) +} diff --git a/internal/acl/acl_test.go b/internal/acl/acl_test.go new file mode 100644 index 00000000..5b1b30e7 --- /dev/null +++ b/internal/acl/acl_test.go @@ -0,0 +1,276 @@ +package acl_test + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path" + "reflect" + "strconv" + "testing" + + "hakurei.app/internal/acl" +) + +const testFileName = "acl.test" + +var ( + uid = os.Geteuid() + cred = int32(os.Geteuid()) +) + +func TestUpdate(t *testing.T) { + if os.Getenv("GO_TEST_SKIP_ACL") == "1" { + t.Log("acl test skipped") + t.SkipNow() + } + + testFilePath := path.Join(t.TempDir(), testFileName) + + if f, err := os.Create(testFilePath); err != nil { + t.Fatalf("Create: error = %v", err) + } else { + if err = f.Close(); err != nil { + t.Fatalf("Close: error = %v", err) + } + } + defer func() { + if err := os.Remove(testFilePath); err != nil { + t.Fatalf("Remove: error = %v", err) + } + }() + + cur := getfacl(t, testFilePath) + + t.Run("default entry count", func(t *testing.T) { + if len(cur) != 3 { + t.Fatalf("unexpected test file acl length %d", len(cur)) + } + }) + + t.Run("default clear mask", func(t *testing.T) { + if err := acl.Update(testFilePath, uid); err != nil { + t.Fatalf("Update: error = %v", err) + } + if cur = getfacl(t, testFilePath); len(cur) != 4 { + t.Fatalf("Update: %v", cur) + } + }) + + t.Run("default clear consistency", func(t *testing.T) { + if err := acl.Update(testFilePath, uid); err != nil { + t.Fatalf("Update: error = %v", err) + } + if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) { + t.Fatalf("Update: %v, want %v", val, cur) + } + }) + + testUpdate(t, testFilePath, "r--", cur, fAclPermRead, acl.Read) + testUpdate(t, testFilePath, "-w-", cur, fAclPermWrite, acl.Write) + testUpdate(t, testFilePath, "--x", cur, fAclPermExecute, acl.Execute) + testUpdate(t, testFilePath, "-wx", cur, fAclPermWrite|fAclPermExecute, acl.Write, acl.Execute) + testUpdate(t, testFilePath, "r-x", cur, fAclPermRead|fAclPermExecute, acl.Read, acl.Execute) + testUpdate(t, testFilePath, "rw-", cur, fAclPermRead|fAclPermWrite, acl.Read, acl.Write) + testUpdate(t, testFilePath, "rwx", cur, fAclPermRead|fAclPermWrite|fAclPermExecute, acl.Read, acl.Write, acl.Execute) +} + +func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val fAclPerm, perms ...acl.Perm) { + t.Run(name, func(t *testing.T) { + t.Cleanup(func() { + if err := acl.Update(testFilePath, uid); err != nil { + t.Fatalf("Update: error = %v", err) + } + if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) { + t.Fatalf("Update: %v, want %v", v, cur) + } + }) + + if err := acl.Update(testFilePath, uid, perms...); err != nil { + t.Fatalf("Update: error = %v", err) + } + r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred) + if r == nil { + t.Fatalf("Update did not add an ACL entry") + } + if !r.equals(fAclTypeUser, cred, val) { + t.Fatalf("Update(%s) = %s", name, r) + } + }) +} + +type ( + getFAclInvocation struct { + cmd *exec.Cmd + val []*getFAclResp + pe []error + } + + getFAclResp struct { + typ fAclType + cred int32 + val fAclPerm + + raw []byte + } + + fAclPerm uintptr + fAclType uint8 +) + +const fAclBufSize = 16 + +const ( + fAclPermRead fAclPerm = 1 << iota + fAclPermWrite + fAclPermExecute +) + +const ( + fAclTypeUser fAclType = iota + fAclTypeGroup + fAclTypeMask + fAclTypeOther +) + +func (c *getFAclInvocation) run(name string) error { + if c.cmd != nil { + panic("attempted to run twice") + } + + c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name) + + scanErr := make(chan error, 1) + if p, err := c.cmd.StdoutPipe(); err != nil { + return err + } else { + go c.parse(p, scanErr) + } + + if err := c.cmd.Start(); err != nil { + return err + } + + return errors.Join(<-scanErr, c.cmd.Wait()) +} + +func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) { + c.val = make([]*getFAclResp, 0, 4+fAclBufSize) + + s := bufio.NewScanner(pipe) + for s.Scan() { + fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3) + if len(fields) != 3 { + continue + } + + resp := getFAclResp{} + + switch string(fields[0]) { + case "user": + resp.typ = fAclTypeUser + case "group": + resp.typ = fAclTypeGroup + case "mask": + resp.typ = fAclTypeMask + case "other": + resp.typ = fAclTypeOther + default: + c.pe = append(c.pe, fmt.Errorf("unknown type %s", string(fields[0]))) + continue + } + + if len(fields[1]) == 0 { + resp.cred = -1 + } else { + if cred, err := strconv.Atoi(string(fields[1])); err != nil { + c.pe = append(c.pe, err) + continue + } else { + resp.cred = int32(cred) + if resp.cred < 0 { + c.pe = append(c.pe, fmt.Errorf("credential %d out of range", resp.cred)) + continue + } + } + } + + if len(fields[2]) != 3 { + c.pe = append(c.pe, fmt.Errorf("invalid perm length %d", len(fields[2]))) + continue + } else { + switch fields[2][0] { + case 'r': + resp.val |= fAclPermRead + case '-': + default: + c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0])) + continue + } + switch fields[2][1] { + case 'w': + resp.val |= fAclPermWrite + case '-': + default: + c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1])) + continue + } + switch fields[2][2] { + case 'x': + resp.val |= fAclPermExecute + case '-': + default: + c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2])) + continue + } + } + + resp.raw = make([]byte, len(s.Bytes())) + copy(resp.raw, s.Bytes()) + c.val = append(c.val, &resp) + } + scanErr <- s.Err() +} + +func (r *getFAclResp) String() string { + if r.raw != nil && len(r.raw) > 0 { + return string(r.raw) + } + + return "(user-initialised resp value)" +} + +func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool { + return r.typ == typ && r.cred == cred && r.val == val +} + +func getfacl(t *testing.T, name string) []*getFAclResp { + c := new(getFAclInvocation) + if err := c.run(name); err != nil { + t.Fatalf("getfacl: error = %v", err) + } + if len(c.pe) != 0 { + t.Errorf("errors encountered parsing getfacl output\n%s", errors.Join(c.pe...).Error()) + } + return c.val +} + +func respByCred(v []*getFAclResp, typ fAclType, cred int32) *getFAclResp { + j := -1 + for i, r := range v { + if r.typ == typ && r.cred == cred { + if j != -1 { + panic("invalid acl") + } + j = i + } + } + if j == -1 { + return nil + } + return v[j] +} diff --git a/internal/acl/libacl-helper.c b/internal/acl/libacl-helper.c new file mode 100644 index 00000000..d1df8167 --- /dev/null +++ b/internal/acl/libacl-helper.c @@ -0,0 +1,90 @@ +#include "libacl-helper.h" +#include +#include +#include +#include + +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; +} diff --git a/internal/acl/libacl-helper.go b/internal/acl/libacl-helper.go new file mode 100644 index 00000000..22575296 --- /dev/null +++ b/internal/acl/libacl-helper.go @@ -0,0 +1,40 @@ +package acl + +import "os" + +func newAclPathError(name string, r int, err error) error { + pathError := &os.PathError{Path: name, Err: err} + switch r { + case 0: + return nil + + case -1: + pathError.Op = "acl_get_file" + case -2: + pathError.Op = "acl_get_tag_type" + case -3: + pathError.Op = "acl_get_qualifier" + case -4: + pathError.Op = "acl_delete_entry" + case -5: + pathError.Op = "acl_create_entry" + case -6: + pathError.Op = "acl_get_permset" + case -7: + pathError.Op = "acl_add_perm" + case -8: + pathError.Op = "acl_set_tag_type" + case -9: + pathError.Op = "acl_set_qualifier" + case -10: + pathError.Op = "acl_calc_mask" + case -11: + pathError.Op = "acl_valid" + case -12: + pathError.Op = "acl_set_file" + + default: // unreachable + pathError.Op = "setfacl" + } + return pathError +} diff --git a/internal/acl/libacl-helper.h b/internal/acl/libacl-helper.h new file mode 100644 index 00000000..b86eb170 --- /dev/null +++ b/internal/acl/libacl-helper.h @@ -0,0 +1,4 @@ +#include + +int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid, + acl_perm_t *perms, size_t plen); diff --git a/internal/acl/libacl-helper_test.go b/internal/acl/libacl-helper_test.go new file mode 100644 index 00000000..13d23a3c --- /dev/null +++ b/internal/acl/libacl-helper_test.go @@ -0,0 +1,60 @@ +package acl + +import ( + "os" + "reflect" + "syscall" + "testing" + + "hakurei.app/container" +) + +func TestNewAclPathError(t *testing.T) { + testCases := []struct { + name string + path string + r int + err error + want error + }{ + {"nil", container.Nonexistent, 0, syscall.ENOTRECOVERABLE, nil}, + + {"acl_get_file", container.Nonexistent, -1, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_get_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_get_tag_type", container.Nonexistent, -2, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_get_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_get_qualifier", container.Nonexistent, -3, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_get_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_delete_entry", container.Nonexistent, -4, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_delete_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_create_entry", container.Nonexistent, -5, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_create_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_get_permset", container.Nonexistent, -6, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_get_permset", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_add_perm", container.Nonexistent, -7, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_add_perm", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_set_tag_type", container.Nonexistent, -8, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_set_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_set_qualifier", container.Nonexistent, -9, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_set_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_calc_mask", container.Nonexistent, -10, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_calc_mask", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_valid", container.Nonexistent, -11, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_valid", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_set_file", container.Nonexistent, -12, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_set_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + + {"acl", container.Nonexistent, -13, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "setfacl", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"invalid", container.Nonexistent, -0xdead, nil, + &os.PathError{Op: "setfacl", Path: container.Nonexistent}}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := newAclPathError(tc.path, tc.r, tc.err) + if !reflect.DeepEqual(err, tc.want) { + t.Errorf("newAclPathError: %v, want %v", err, tc.want) + } + }) + } +} diff --git a/internal/acl/perms.go b/internal/acl/perms.go new file mode 100644 index 00000000..fbc99e82 --- /dev/null +++ b/internal/acl/perms.go @@ -0,0 +1,18 @@ +package acl + +type Perms []Perm + +func (ps Perms) String() string { + var s = []byte("---") + for _, p := range ps { + switch p { + case Read: + s[0] = 'r' + case Write: + s[1] = 'w' + case Execute: + s[2] = 'x' + } + } + return string(s) +} diff --git a/internal/acl/perms_test.go b/internal/acl/perms_test.go new file mode 100644 index 00000000..3920046d --- /dev/null +++ b/internal/acl/perms_test.go @@ -0,0 +1,30 @@ +package acl_test + +import ( + "testing" + + "hakurei.app/internal/acl" +) + +func TestPerms(t *testing.T) { + testCases := []struct { + name string + perms acl.Perms + }{ + {"---", acl.Perms{}}, + {"r--", acl.Perms{acl.Read}}, + {"-w-", acl.Perms{acl.Write}}, + {"--x", acl.Perms{acl.Execute}}, + {"rw-", acl.Perms{acl.Read, acl.Read, acl.Write}}, + {"r-x", acl.Perms{acl.Read, acl.Execute, acl.Execute}}, + {"-wx", acl.Perms{acl.Write, acl.Write, acl.Execute, acl.Execute}}, + {"rwx", acl.Perms{acl.Read, acl.Write, acl.Execute}}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.perms.String(); got != tc.name { + t.Errorf("String: %q, want %q", got, tc.name) + } + }) + } +} diff --git a/internal/dbus/address.go b/internal/dbus/address.go new file mode 100644 index 00000000..edee7e20 --- /dev/null +++ b/internal/dbus/address.go @@ -0,0 +1,193 @@ +package dbus + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "slices" +) + +type AddrEntry struct { + Method string `json:"method"` + Values [][2]string `json:"values"` +} + +// EqualAddrEntries returns whether two slices of [AddrEntry] are equal. +func EqualAddrEntries(entries, target []AddrEntry) bool { + return slices.EqualFunc(entries, target, func(a AddrEntry, b AddrEntry) bool { + return a.Method == b.Method && slices.Equal(a.Values, b.Values) + }) +} + +// Parse parses D-Bus address according to +// https://dbus.freedesktop.org/doc/dbus-specification.html#addresses +func Parse(addr []byte) ([]AddrEntry, error) { + // Look for a semicolon + address := bytes.Split(bytes.TrimSuffix(addr, []byte{';'}), []byte{';'}) + + // Allocate for entries + v := make([]AddrEntry, len(address)) + + for i, s := range address { + var pairs [][]byte + + // Look for the colon : + if method, list, ok := bytes.Cut(s, []byte{':'}); !ok { + return v, &BadAddressError{ErrNoColon, i, s, -1, nil} + } else { + pairs = bytes.Split(list, []byte{','}) + v[i].Method = string(method) + v[i].Values = make([][2]string, len(pairs)) + } + + for j, pair := range pairs { + key, value, ok := bytes.Cut(pair, []byte{'='}) + if !ok { + return v, &BadAddressError{ErrBadPairSep, i, s, j, pair} + } + if len(key) == 0 { + return v, &BadAddressError{ErrBadPairKey, i, s, j, pair} + } + if len(value) == 0 { + return v, &BadAddressError{ErrBadPairVal, i, s, j, pair} + } + v[i].Values[j][0] = string(key) + + if val, errno := unescapeValue(value); errno != errSuccess { + return v, &BadAddressError{errno, i, s, j, pair} + } else { + v[i].Values[j][1] = string(val) + } + } + } + + return v, nil +} + +func unescapeValue(v []byte) (val []byte, errno ParseError) { + if l := len(v) - (bytes.Count(v, []byte{'%'}) * 2); l < 0 { + errno = ErrBadValLength + return + } else { + val = make([]byte, l) + } + + var i, skip int + for iu, b := range v { + if skip > 0 { + skip-- + continue + } + + if ib := bytes.IndexByte([]byte("-_/.\\*"), b); ib != -1 { // - // _/.\* + goto opt + } else if b >= '0' && b <= '9' { // 0-9 + goto opt + } else if b >= 'A' && b <= 'Z' { // A-Z + goto opt + } else if b >= 'a' && b <= 'z' { // a-z + goto opt + } + + if b != '%' { + errno = ErrBadValByte + break + } + + skip += 2 + if iu+2 >= len(v) { + errno = ErrBadValHexLength + break + } + if c, err := hex.Decode(val[i:i+1], v[iu+1:iu+3]); err != nil { + if errors.As(err, new(hex.InvalidByteError)) { + errno = ErrBadValHexByte + break + } + // unreachable + panic(err.Error()) + } else if c != 1 { + // unreachable + panic(fmt.Sprintf("invalid decode length %d", c)) + } + i++ + continue + + opt: + val[i] = b + i++ + } + + return +} + +type ParseError uint8 + +func (e ParseError) Error() string { + switch e { + case errSuccess: + panic("attempted to return success as error") + case ErrNoColon: + return "address does not contain a colon" + case ErrBadPairSep: + return "'=' character not found" + case ErrBadPairKey: + return "'=' character has no key preceding it" + case ErrBadPairVal: + return "'=' character has no value following it" + case ErrBadValLength: + return "unescaped value has impossible length" + case ErrBadValByte: + return "in D-Bus address, characters other than [-0-9A-Za-z_/.\\*] should have been escaped" + case ErrBadValHexLength: + return "in D-Bus address, percent character was not followed by two hex digits" + case ErrBadValHexByte: + return "in D-Bus address, percent character was followed by characters other than hex digits" + + default: + return fmt.Sprintf("parse error %d", e) + } +} + +const ( + errSuccess ParseError = iota + ErrNoColon + ErrBadPairSep + ErrBadPairKey + ErrBadPairVal + ErrBadValLength + ErrBadValByte + ErrBadValHexLength + ErrBadValHexByte +) + +type BadAddressError struct { + // error type + Type ParseError + + // bad entry position + EntryPos int + // bad entry value + EntryVal []byte + + // bad pair position + PairPos int + // bad pair value + PairVal []byte +} + +func (a *BadAddressError) Is(err error) bool { + var b *BadAddressError + return errors.As(err, &b) && a.Type == b.Type && + a.EntryPos == b.EntryPos && slices.Equal(a.EntryVal, b.EntryVal) && + a.PairPos == b.PairPos && slices.Equal(a.PairVal, b.PairVal) +} + +func (a *BadAddressError) Error() string { + return a.Type.Error() +} + +func (a *BadAddressError) Unwrap() error { + return a.Type +} diff --git a/internal/dbus/address_escape_test.go b/internal/dbus/address_escape_test.go new file mode 100644 index 00000000..4ef48f8b --- /dev/null +++ b/internal/dbus/address_escape_test.go @@ -0,0 +1,59 @@ +package dbus + +import ( + "testing" +) + +func TestUnescapeValue(t *testing.T) { + t.Parallel() + + testCases := []struct { + value string + want string + wantErr ParseError + }{ + // upstream test cases + {value: "abcde", want: "abcde"}, + {value: "", want: ""}, + {value: "%20%20", want: " "}, + {value: "%24", want: "$"}, + {value: "%25", want: "%"}, + {value: "abc%24", want: "abc$"}, + {value: "%24abc", want: "$abc"}, + {value: "abc%24abc", want: "abc$abc"}, + {value: "/", want: "/"}, + {value: "-", want: "-"}, + {value: "_", want: "_"}, + {value: "A", want: "A"}, + {value: "I", want: "I"}, + {value: "Z", want: "Z"}, + {value: "a", want: "a"}, + {value: "i", want: "i"}, + {value: "z", want: "z"}, + /* Bug: https://bugs.freedesktop.org/show_bug.cgi?id=53499 */ + {value: "%c3%b6", want: "\xc3\xb6"}, + + {value: "%a", wantErr: ErrBadValHexLength}, + {value: "%q", wantErr: ErrBadValHexLength}, + {value: "%az", wantErr: ErrBadValHexByte}, + {value: "%%", wantErr: ErrBadValLength}, + {value: "%$$", wantErr: ErrBadValHexByte}, + {value: "abc%a", wantErr: ErrBadValHexLength}, + {value: "%axyz", wantErr: ErrBadValHexByte}, + {value: "%", wantErr: ErrBadValLength}, + {value: "$", wantErr: ErrBadValByte}, + {value: " ", wantErr: ErrBadValByte}, + } + + for _, tc := range testCases { + t.Run("unescape "+tc.value, func(t *testing.T) { + t.Parallel() + + if got, errno := unescapeValue([]byte(tc.value)); errno != tc.wantErr { + t.Errorf("unescapeValue() errno = %v, wantErr %v", errno, tc.wantErr) + } else if tc.wantErr == errSuccess && string(got) != tc.want { + t.Errorf("unescapeValue() = %q, want %q", got, tc.want) + } + }) + } +} diff --git a/internal/dbus/address_test.go b/internal/dbus/address_test.go new file mode 100644 index 00000000..7517aa1d --- /dev/null +++ b/internal/dbus/address_test.go @@ -0,0 +1,123 @@ +package dbus_test + +import ( + "errors" + "reflect" + "testing" + + "hakurei.app/internal/dbus" +) + +func TestParse(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + addr string + want []dbus.AddrEntry + wantErr error + }{ + { + name: "simple session unix", + addr: "unix:path=/run/user/1971/bus", + want: []dbus.AddrEntry{{ + Method: "unix", + Values: [][2]string{{"path", "/run/user/1971/bus"}}, + }}, + }, + { + name: "simple upper escape", + addr: "debug:name=Test,cat=cute,escaped=%c3%b6", + want: []dbus.AddrEntry{{ + Method: "debug", + Values: [][2]string{ + {"name", "Test"}, + {"cat", "cute"}, + {"escaped", "\xc3\xb6"}, + }, + }}, + }, + { + name: "simple bad escape", + addr: "debug:name=%", + wantErr: &dbus.BadAddressError{Type: dbus.ErrBadValLength, + EntryPos: 0, EntryVal: []byte("debug:name=%"), PairPos: 0, PairVal: []byte("name=%")}, + }, + + // upstream test cases + { + name: "full address success", + addr: "unix:path=/tmp/foo;debug:name=test,sliff=sloff;", + want: []dbus.AddrEntry{ + {Method: "unix", Values: [][2]string{{"path", "/tmp/foo"}}}, + {Method: "debug", Values: [][2]string{{"name", "test"}, {"sliff", "sloff"}}}, + }, + }, + { + name: "empty address", + addr: "", + wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon, + EntryVal: []byte{}, PairPos: -1}, + }, + { + name: "no body", + addr: "foo", + wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon, + EntryPos: 0, EntryVal: []byte("foo"), PairPos: -1}, + }, + { + name: "no pair separator", + addr: "foo:bar", + wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep, + EntryPos: 0, EntryVal: []byte("foo:bar"), PairPos: 0, PairVal: []byte("bar")}, + }, + { + name: "no pair separator multi pair", + addr: "foo:bar,baz", + wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep, + EntryPos: 0, EntryVal: []byte("foo:bar,baz"), PairPos: 0, PairVal: []byte("bar")}, + }, + { + name: "no pair separator single valid pair", + addr: "foo:bar=foo,baz", + wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep, + EntryPos: 0, EntryVal: []byte("foo:bar=foo,baz"), PairPos: 1, PairVal: []byte("baz")}, + }, + { + name: "no body single valid address", + addr: "foo:bar=foo;baz", + wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon, + EntryPos: 1, EntryVal: []byte("baz"), PairPos: -1}, + }, + { + name: "no key", + addr: "foo:=foo", + wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairKey, + EntryPos: 0, EntryVal: []byte("foo:=foo"), PairPos: 0, PairVal: []byte("=foo")}, + }, + { + name: "no value", + addr: "foo:foo=", + wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairVal, + EntryPos: 0, EntryVal: []byte("foo:foo="), PairPos: 0, PairVal: []byte("foo=")}, + }, + { + name: "no pair separator single valid pair trailing", + addr: "foo:foo,bar=baz", + wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep, + EntryPos: 0, EntryVal: []byte("foo:foo,bar=baz"), PairPos: 0, PairVal: []byte("foo")}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if got, err := dbus.Parse([]byte(tc.addr)); !errors.Is(err, tc.wantErr) { + t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr) + } else if tc.wantErr == nil && !reflect.DeepEqual(got, tc.want) { + t.Errorf("Parse() = %#v, want %#v", got, tc.want) + } + }) + } +} diff --git a/internal/dbus/config.go b/internal/dbus/config.go new file mode 100644 index 00000000..2a09ee90 --- /dev/null +++ b/internal/dbus/config.go @@ -0,0 +1,71 @@ +package dbus + +import ( + "hakurei.app/hst" +) + +// ProxyPair is an upstream dbus address and a downstream socket path. +type ProxyPair [2]string + +// Args returns the xdg-dbus-proxy arguments equivalent of [hst.BusConfig]. +func Args(c *hst.BusConfig, bus ProxyPair) (args []string) { + argc := 2 + len(c.See) + len(c.Talk) + len(c.Own) + len(c.Call) + len(c.Broadcast) + if c.Log { + argc++ + } + if c.Filter { + argc++ + } + + args = make([]string, 0, argc) + args = append(args, bus[0], bus[1]) + if c.Filter { + args = append(args, "--filter") + } + for _, name := range c.See { + args = append(args, "--see="+name) + } + for _, name := range c.Talk { + args = append(args, "--talk="+name) + } + for _, name := range c.Own { + args = append(args, "--own="+name) + } + for name, rule := range c.Call { + args = append(args, "--call="+name+"="+rule) + } + for name, rule := range c.Broadcast { + args = append(args, "--broadcast="+name+"="+rule) + } + if c.Log { + args = append(args, "--log") + } + + return +} + +// NewConfig returns the address of a new [hst.BusConfig] with optional defaults. +func NewConfig(id string, defaults, mpris bool) *hst.BusConfig { + c := hst.BusConfig{ + Call: make(map[string]string), + Broadcast: make(map[string]string), + + Filter: true, + } + + if defaults { + c.Talk = []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"} + + c.Call["org.freedesktop.portal.*"] = "*" + c.Broadcast["org.freedesktop.portal.*"] = "@/org/freedesktop/portal/*" + + if id != "" { + c.Own = []string{id + ".*"} + if mpris { + c.Own = append(c.Own, "org.mpris.MediaPlayer2."+id+".*") + } + } + } + + return &c +} diff --git a/internal/dbus/config_test.go b/internal/dbus/config_test.go new file mode 100644 index 00000000..8509007a --- /dev/null +++ b/internal/dbus/config_test.go @@ -0,0 +1,124 @@ +package dbus_test + +import ( + "reflect" + "slices" + "strings" + "testing" + + "hakurei.app/hst" + "hakurei.app/internal/dbus" +) + +func TestConfigArgs(t *testing.T) { + t.Parallel() + + for _, tc := range testCasesExt { + if tc.wantErr { + // args does not check for nulls + continue + } + + t.Run("build arguments for "+tc.id, func(t *testing.T) { + t.Parallel() + + if got := dbus.Args(tc.c, tc.bus); !slices.Equal(got, tc.want) { + t.Errorf("Args: %v, want %v", got, tc.want) + } + }) + } +} + +func TestNewConfig(t *testing.T) { + t.Parallel() + ids := [...]string{"org.chromium.Chromium", "dev.vencord.Vesktop"} + + type newTestCase struct { + id string + args [2]bool + want *hst.BusConfig + } + + // populate tests from IDs in generic tests + tcs := make([]newTestCase, 0, (len(ids)+1)*4) + // tests for defaults without id + tcs = append(tcs, + newTestCase{"", [2]bool{false, false}, &hst.BusConfig{ + Call: make(map[string]string), + Broadcast: make(map[string]string), + Filter: true, + }}, + newTestCase{"", [2]bool{false, true}, &hst.BusConfig{ + Call: make(map[string]string), + Broadcast: make(map[string]string), + Filter: true, + }}, + newTestCase{"", [2]bool{true, false}, &hst.BusConfig{ + Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}, + Call: map[string]string{"org.freedesktop.portal.*": "*"}, + Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, + Filter: true, + }}, + newTestCase{"", [2]bool{true, true}, &hst.BusConfig{ + Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}, + Call: map[string]string{"org.freedesktop.portal.*": "*"}, + Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, + Filter: true, + }}, + ) + for _, id := range ids { + tcs = append(tcs, + newTestCase{id, [2]bool{false, false}, &hst.BusConfig{ + Call: make(map[string]string), + Broadcast: make(map[string]string), + Filter: true, + }}, + newTestCase{id, [2]bool{false, true}, &hst.BusConfig{ + Call: make(map[string]string), + Broadcast: make(map[string]string), + Filter: true, + }}, + newTestCase{id, [2]bool{true, false}, &hst.BusConfig{ + Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}, + Own: []string{id + ".*"}, + Call: map[string]string{"org.freedesktop.portal.*": "*"}, + Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, + Filter: true, + }}, + newTestCase{id, [2]bool{true, true}, &hst.BusConfig{ + Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}, + Own: []string{id + ".*", "org.mpris.MediaPlayer2." + id + ".*"}, + Call: map[string]string{"org.freedesktop.portal.*": "*"}, + Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, + Filter: true, + }}, + ) + } + + for _, tc := range tcs { + name := new(strings.Builder) + name.WriteString("create new configuration struct") + + if tc.args[0] { + name.WriteString(" with builtin defaults") + if tc.args[1] { + name.WriteString(" (mpris)") + } + } + + if tc.id != "" { + name.WriteString(" for application ID ") + name.WriteString(tc.id) + } + + t.Run(name.String(), func(t *testing.T) { + t.Parallel() + + if gotC := dbus.NewConfig(tc.id, tc.args[0], tc.args[1]); !reflect.DeepEqual(gotC, tc.want) { + t.Errorf("NewConfig(%q, %t, %t) = %v, want %v", + tc.id, tc.args[0], tc.args[1], + gotC, tc.want) + } + }) + } +} diff --git a/internal/dbus/dbus.go b/internal/dbus/dbus.go new file mode 100644 index 00000000..9ea89f51 --- /dev/null +++ b/internal/dbus/dbus.go @@ -0,0 +1,69 @@ +// Package dbus wraps xdg-dbus-proxy and implements configuration and sandboxing of the underlying helper process. +package dbus + +import ( + "fmt" + "os" + "sync" +) + +const ( + /* + SessionBusAddress is the name of the environment variable where the address of the login session message bus is given in. + + If that variable is not set, applications may also try to read the address from the X Window System root window property _DBUS_SESSION_BUS_ADDRESS. + The root window property must have type STRING. The environment variable should have precedence over the root window property. + + The address of the login session message bus is given in the DBUS_SESSION_BUS_ADDRESS environment variable. + If DBUS_SESSION_BUS_ADDRESS is not set, or if it's set to the string "autolaunch:", + the system should use platform-specific methods of locating a running D-Bus session server, + or starting one if a running instance cannot be found. + Note that this mechanism is not recommended for attempting to determine if a daemon is running. + It is inherently racy to attempt to make this determination, since the bus daemon may be started just before or just after the determination is made. + Therefore, it is recommended that applications do not try to make this determination for their functionality purposes, and instead they should attempt to start the server. + + This package diverges from the specification, as the caller is unlikely to be an X client, or be in a position to autolaunch a dbus server. + So a fallback address with a socket located in the well-known default XDG_RUNTIME_DIR formatting is used. + */ + SessionBusAddress = "DBUS_SESSION_BUS_ADDRESS" + + /* + SystemBusAddress is the name of the environment variable where the address of the system message bus is given in. + + If that variable is not set, applications should try to connect to the well-known address unix:path=/var/run/dbus/system_bus_socket. + Implementations of the well-known system bus should listen on an address that will result in that connection being successful. + */ + SystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS" + + // FallbackSystemBusAddress is used when [SystemBusAddress] is not set. + FallbackSystemBusAddress = "unix:path=/var/run/dbus/system_bus_socket" +) + +var ( + address [2]string + addressOnce sync.Once +) + +// Address returns the session and system bus addresses copied from environment, +// or appropriate fallback values if they are not set. +func Address() (session, system string) { + addressOnce.Do(func() { + // resolve upstream session bus address + if addr, ok := os.LookupEnv(SessionBusAddress); !ok { + // fall back to default format + address[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid()) + } else { + address[0] = addr + } + + // resolve upstream system bus address + if addr, ok := os.LookupEnv(SystemBusAddress); !ok { + // fall back to default hardcoded value + address[1] = FallbackSystemBusAddress + } else { + address[1] = addr + } + }) + + return address[0], address[1] +} diff --git a/internal/dbus/dbus_test.go b/internal/dbus/dbus_test.go new file mode 100644 index 00000000..5d6e4c40 --- /dev/null +++ b/internal/dbus/dbus_test.go @@ -0,0 +1,170 @@ +package dbus_test + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "strings" + "syscall" + "testing" + "time" + + "hakurei.app/internal/dbus" + "hakurei.app/internal/helper" + "hakurei.app/message" +) + +func TestFinalise(t *testing.T) { + if _, err := dbus.Finalise(dbus.ProxyPair{}, dbus.ProxyPair{}, nil, nil); !errors.Is(err, syscall.EBADE) { + t.Errorf("Finalise: error = %v, want %v", + err, syscall.EBADE) + } + + for id, tc := range testCasePairs { + t.Run("create final for "+id, func(t *testing.T) { + var wt io.WriterTo + if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr { + t.Errorf("Finalise: error = %v, wantErr %v", + err, tc[0].wantErr) + return + } else { + wt = v + } + + // rest of the tests happen for sealed instances + if tc[0].wantErr { + return + } + + // build null-terminated string from wanted args + want := new(strings.Builder) + args := append(tc[0].want, tc[1].want...) + for _, arg := range args { + want.WriteString(arg) + want.WriteByte(0) + } + + got := new(strings.Builder) + if _, err := wt.WriteTo(got); err != nil { + t.Errorf("WriteTo: error = %v", err) + } + + if want.String() != got.String() { + t.Errorf("Seal: %q, want %q", + got.String(), want.String()) + } + }) + } +} + +func TestProxyStartWaitCloseString(t *testing.T) { + t.Run("sandbox", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, true) }) + t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) }) +} + +const ( + stubProxyTimeout = 5 * time.Second +) + +func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { + { + oldWaitDelay := helper.WaitDelay + helper.WaitDelay = 16 * time.Second + t.Cleanup(func() { helper.WaitDelay = oldWaitDelay }) + } + + { + proxyName := dbus.ProxyName + dbus.ProxyName = os.Args[0] + t.Cleanup(func() { dbus.ProxyName = proxyName }) + } + + var p *dbus.Proxy + + t.Run("string for nil proxy", func(t *testing.T) { + want := "(invalid dbus proxy)" + if got := p.String(); got != want { + t.Errorf("String: %q, want %q", + got, want) + } + }) + + t.Run("invalid start", func(t *testing.T) { + if !useSandbox { + p = dbus.NewDirect(t.Context(), message.New(nil), nil, nil) + } else { + p = dbus.New(t.Context(), message.New(nil), nil, nil) + } + + if err := p.Start(); !errors.Is(err, syscall.ENOTRECOVERABLE) { + t.Errorf("Start: error = %q, wantErr %q", err, syscall.ENOTRECOVERABLE) + return + } + }) + + for id, tc := range testCasePairs { + // this test does not test errors + if tc[0].wantErr { + continue + } + + t.Run("proxy for "+id, func(t *testing.T) { + var final *dbus.Final + t.Run("finalise", func(t *testing.T) { + if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); err != nil { + t.Errorf("Finalise: error = %v, wantErr %v", err, tc[0].wantErr) + return + } else { + final = v + } + }) + + ctx, cancel := context.WithTimeout(t.Context(), stubProxyTimeout) + defer cancel() + output := new(strings.Builder) + if !useSandbox { + p = dbus.NewDirect(ctx, message.New(nil), final, output) + } else { + p = dbus.New(ctx, message.New(nil), final, output) + } + + { // check invalid wait behaviour + wantErr := "dbus: not started" + if err := p.Wait(); err == nil || err.Error() != wantErr { + t.Errorf("Wait: error = %v, wantErr %v", err, wantErr) + } + } + + { // check string behaviour + want := "(unused dbus proxy)" + if got := p.String(); got != want { + t.Errorf("String: %q, want %q", got, want) + return + } + } + + if err := p.Start(); err != nil { + t.Fatalf("Start: error = %v", err) + } + + { // check running string behaviour + wantSubstr := fmt.Sprintf("%s --args=3 --fd=4", os.Args[0]) + if useSandbox { + wantSubstr = `argv: ["xdg-dbus-proxy" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf` + } + if got := p.String(); !strings.Contains(got, wantSubstr) { + t.Errorf("String: %q, want %q", + got, wantSubstr) + return + } + } + + p.Close() + if err := p.Wait(); err != nil { + t.Errorf("Wait: error = %v\noutput: %s", err, output.String()) + } + }) + } +} diff --git a/internal/dbus/export_test.go b/internal/dbus/export_test.go new file mode 100644 index 00000000..bc24f3f3 --- /dev/null +++ b/internal/dbus/export_test.go @@ -0,0 +1,15 @@ +package dbus + +import ( + "context" + "io" + + "hakurei.app/message" +) + +// NewDirect returns a new instance of [Proxy] with its sandbox disabled. +func NewDirect(ctx context.Context, msg message.Msg, final *Final, output io.Writer) *Proxy { + p := New(ctx, msg, final, output) + p.useSandbox = false + return p +} diff --git a/internal/dbus/proc.go b/internal/dbus/proc.go new file mode 100644 index 00000000..c934e89e --- /dev/null +++ b/internal/dbus/proc.go @@ -0,0 +1,188 @@ +package dbus + +import ( + "context" + "errors" + "os" + "os/exec" + "strconv" + "syscall" + + "hakurei.app/container" + "hakurei.app/container/check" + "hakurei.app/container/seccomp" + "hakurei.app/container/std" + "hakurei.app/internal/helper" + "hakurei.app/ldd" +) + +// Start starts and configures a D-Bus proxy process. +func (p *Proxy) Start() error { + if p.final == nil || p.final.WriterTo == nil { + return syscall.ENOTRECOVERABLE + } + + p.mu.Lock() + defer p.mu.Unlock() + p.pmu.Lock() + defer p.pmu.Unlock() + + if p.cancel != nil || p.cause != nil { + return errors.New("dbus: already started") + } + + ctx, cancel := context.WithCancelCause(p.ctx) + + if !p.useSandbox { + p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) { + if p.output != nil { + cmd.Stdout, cmd.Stderr = p.output, p.output + } + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Env = make([]string, 0) + }, nil) + } else { + var toolPath *check.Absolute + if a, err := check.NewAbs(p.name); err != nil { + if p.name, err = exec.LookPath(p.name); err != nil { + return err + } else if toolPath, err = check.NewAbs(p.name); err != nil { + return err + } + } else { + toolPath = a + } + + var libPaths []*check.Absolute + if entries, err := ldd.Resolve(ctx, p.msg, toolPath); err != nil { + return err + } else { + libPaths = ldd.Path(entries) + } + + p.helper = helper.New( + ctx, p.msg, toolPath, "xdg-dbus-proxy", + p.final, true, + argF, func(z *container.Container) { + z.SeccompFlags |= seccomp.AllowMultiarch + z.SeccompPresets |= std.PresetStrict + z.Hostname = "hakurei-dbus" + if p.output != nil { + z.Stdout, z.Stderr = p.output, p.output + } + + // these lib paths are unpredictable, so mount them first so they cannot cover anything + for _, name := range libPaths { + z.Bind(name, name, 0) + } + + // upstream bus directories + upstreamPaths := make([]*check.Absolute, 0, 2) + for _, addr := range [][]AddrEntry{p.final.SessionUpstream, p.final.SystemUpstream} { + for _, ent := range addr { + if ent.Method != "unix" { + continue + } + for _, pair := range ent.Values { + if pair[0] != "path" { + continue + } + if a, err := check.NewAbs(pair[1]); err != nil { + continue + } else { + upstreamPaths = append(upstreamPaths, a.Dir()) + } + } + } + } + check.SortAbs(upstreamPaths) + upstreamPaths = check.CompactAbs(upstreamPaths) + for _, name := range upstreamPaths { + z.Bind(name, name, 0) + } + z.HostNet = len(upstreamPaths) == 0 + z.HostAbstract = z.HostNet + + // parent directories of bind paths + sockDirPaths := make([]*check.Absolute, 0, 2) + if a, err := check.NewAbs(p.final.Session[1]); err == nil { + sockDirPaths = append(sockDirPaths, a.Dir()) + } + if a, err := check.NewAbs(p.final.System[1]); err == nil { + sockDirPaths = append(sockDirPaths, a.Dir()) + } + check.SortAbs(sockDirPaths) + sockDirPaths = check.CompactAbs(sockDirPaths) + for _, name := range sockDirPaths { + z.Bind(name, name, std.BindWritable) + } + + // xdg-dbus-proxy bin path + binPath := toolPath.Dir() + z.Bind(binPath, binPath, 0) + }, nil) + } + + if err := p.helper.Start(); err != nil { + cancel(err) + p.helper = nil + return err + } + + p.cancel, p.cause = cancel, func() error { return context.Cause(ctx) } + return nil +} + +var proxyClosed = errors.New("proxy closed") + +// Wait blocks until xdg-dbus-proxy exits and releases resources. +func (p *Proxy) Wait() error { + p.mu.RLock() + defer p.mu.RUnlock() + + p.pmu.RLock() + if p.helper == nil || p.cancel == nil || p.cause == nil { + p.pmu.RUnlock() + return errors.New("dbus: not started") + } + + var errs [3]error + + errs[0] = p.helper.Wait() + if errors.Is(errs[0], context.Canceled) && + errors.Is(p.cause(), proxyClosed) { + errs[0] = nil + } + p.pmu.RUnlock() + + // ensure socket removal so ephemeral directory is empty at revert + if err := os.Remove(p.final.Session[1]); err != nil && !errors.Is(err, os.ErrNotExist) { + errs[1] = err + } + if p.final.System[1] != "" { + if err := os.Remove(p.final.System[1]); err != nil && !errors.Is(err, os.ErrNotExist) { + errs[2] = err + } + } + + return errors.Join(errs[:]...) +} + +// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy. +func (p *Proxy) Close() { + p.pmu.Lock() + defer p.pmu.Unlock() + + if p.cancel == nil { + panic("dbus: not started") + } + p.cancel(proxyClosed) +} + +func argF(argsFd, statFd int) []string { + if statFd == -1 { + return []string{"--args=" + strconv.Itoa(argsFd)} + } else { + return []string{"--args=" + strconv.Itoa(argsFd), "--fd=" + strconv.Itoa(statFd)} + } +} diff --git a/internal/dbus/proc_test.go b/internal/dbus/proc_test.go new file mode 100644 index 00000000..544856ed --- /dev/null +++ b/internal/dbus/proc_test.go @@ -0,0 +1,11 @@ +package dbus_test + +import ( + "os" + "testing" + + "hakurei.app/container" + "hakurei.app/internal/helper" +) + +func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) } diff --git a/internal/dbus/proxy.go b/internal/dbus/proxy.go new file mode 100644 index 00000000..9de49c41 --- /dev/null +++ b/internal/dbus/proxy.go @@ -0,0 +1,105 @@ +package dbus + +import ( + "context" + "io" + "sync" + "syscall" + + "hakurei.app/hst" + "hakurei.app/internal/helper" + "hakurei.app/message" +) + +// ProxyName is the file name or path to the proxy program. +// Overriding ProxyName will only affect Proxy instance created after the change. +var ProxyName = "xdg-dbus-proxy" + +// Proxy holds the state of a xdg-dbus-proxy process, and should never be copied. +type Proxy struct { + helper helper.Helper + ctx context.Context + msg message.Msg + + cancel context.CancelCauseFunc + cause func() error + + final *Final + output io.Writer + useSandbox bool + + name string + + mu, pmu sync.RWMutex +} + +func (p *Proxy) String() string { + if p == nil { + return "(invalid dbus proxy)" + } + + p.mu.RLock() + defer p.mu.RUnlock() + + if p.helper != nil { + return p.helper.String() + } + + return "(unused dbus proxy)" +} + +// Final describes the outcome of a proxy configuration. +type Final struct { + Session, System ProxyPair + // parsed upstream address + SessionUpstream, SystemUpstream []AddrEntry + io.WriterTo +} + +// Finalise creates a checked argument writer for [Proxy]. +func Finalise(sessionBus, systemBus ProxyPair, session, system *hst.BusConfig) (final *Final, err error) { + if session == nil && system == nil { + return nil, syscall.EBADE + } + + var args []string + if session != nil { + if err = session.CheckInterfaces("session"); err != nil { + return + } + args = append(args, Args(session, sessionBus)...) + } + if system != nil { + if err = system.CheckInterfaces("system"); err != nil { + return + } + args = append(args, Args(system, systemBus)...) + } + + final = &Final{Session: sessionBus, System: systemBus} + + final.WriterTo, err = helper.NewCheckedArgs(args...) + if err != nil { + return + } + + if session != nil { + final.SessionUpstream, err = Parse([]byte(final.Session[0])) + if err != nil { + return + } + } + if system != nil { + final.SystemUpstream, err = Parse([]byte(final.System[0])) + if err != nil { + return + } + } + + return +} + +// New returns a new instance of [Proxy]. +func New(ctx context.Context, msg message.Msg, final *Final, output io.Writer) *Proxy { + return &Proxy{name: ProxyName, ctx: ctx, msg: msg, final: final, output: output, useSandbox: true} +} diff --git a/internal/dbus/samples_test.go b/internal/dbus/samples_test.go new file mode 100644 index 00000000..7db3d6b9 --- /dev/null +++ b/internal/dbus/samples_test.go @@ -0,0 +1,214 @@ +package dbus_test + +import ( + "hakurei.app/hst" +) + +const ( + sampleHostPath = "/tmp/bus" + sampleHostAddr = "unix:path=" + sampleHostPath + sampleBindPath = "/tmp/proxied_bus" +) + +var samples = []dbusTestCase{ + { + "org.chromium.Chromium", &hst.BusConfig{ + See: nil, + Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver", + "org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"}, + Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*", + "org.mpris.MediaPlayer2.chromium.*"}, + Call: map[string]string{"org.freedesktop.portal.*": "*"}, + Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, + Log: false, + Filter: true, + }, false, false, + [2]string{sampleHostAddr, sampleBindPath}, + []string{ + sampleHostAddr, + sampleBindPath, + "--filter", + "--talk=org.freedesktop.Notifications", + "--talk=org.freedesktop.FileManager1", + "--talk=org.freedesktop.ScreenSaver", + "--talk=org.freedesktop.secrets", + "--talk=org.kde.kwalletd5", + "--talk=org.kde.kwalletd6", + "--talk=org.gnome.SessionManager", + "--own=org.chromium.Chromium.*", + "--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*", + "--own=org.mpris.MediaPlayer2.chromium.*", + "--call=org.freedesktop.portal.*=*", + "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*", + }, + }, + { + "org.chromium.Chromium+", &hst.BusConfig{ + See: nil, + Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"}, + Own: nil, + Call: nil, + Broadcast: nil, + Log: false, + Filter: true, + }, false, false, + [2]string{sampleHostAddr, sampleBindPath}, + []string{ + sampleHostAddr, + sampleBindPath, + "--filter", + "--talk=org.bluez", + "--talk=org.freedesktop.Avahi", + "--talk=org.freedesktop.UPower", + }, + }, + + { + "dev.vencord.Vesktop", &hst.BusConfig{ + See: nil, + Talk: []string{"org.freedesktop.Notifications", "org.kde.StatusNotifierWatcher"}, + Own: []string{"dev.vencord.Vesktop.*", "org.mpris.MediaPlayer2.dev.vencord.Vesktop.*"}, + Call: map[string]string{"org.freedesktop.portal.*": "*"}, + Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, + Log: false, + Filter: true, + }, false, false, + [2]string{sampleHostAddr, sampleBindPath}, + []string{ + sampleHostAddr, + sampleBindPath, + "--filter", + "--talk=org.freedesktop.Notifications", + "--talk=org.kde.StatusNotifierWatcher", + "--own=dev.vencord.Vesktop.*", + "--own=org.mpris.MediaPlayer2.dev.vencord.Vesktop.*", + "--call=org.freedesktop.portal.*=*", + "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*"}, + }, + + { + "uk.gensokyo.CrashTestDummy", &hst.BusConfig{ + See: []string{"uk.gensokyo.CrashTestDummy1"}, + Talk: []string{"org.freedesktop.Notifications"}, + Own: []string{"uk.gensokyo.CrashTestDummy.*", "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*"}, + Call: map[string]string{"org.freedesktop.portal.*": "*"}, + Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, + Log: true, + Filter: true, + }, false, false, + [2]string{sampleHostAddr, sampleBindPath}, + []string{ + sampleHostAddr, + sampleBindPath, + "--filter", + "--see=uk.gensokyo.CrashTestDummy1", + "--talk=org.freedesktop.Notifications", + "--own=uk.gensokyo.CrashTestDummy.*", + "--own=org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*", + "--call=org.freedesktop.portal.*=*", + "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*", + "--log"}, + }, + { + "uk.gensokyo.CrashTestDummy1", &hst.BusConfig{ + See: []string{"uk.gensokyo.CrashTestDummy"}, + Talk: []string{"org.freedesktop.Notifications"}, + Own: []string{"uk.gensokyo.CrashTestDummy1.*", "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy1.*"}, + Call: map[string]string{"org.freedesktop.portal.*": "*"}, + Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, + Log: true, + Filter: true, + }, false, true, + [2]string{sampleHostAddr, sampleBindPath}, + []string{ + sampleHostAddr, + sampleBindPath, + "--filter", + "--see=uk.gensokyo.CrashTestDummy", + "--talk=org.freedesktop.Notifications", + "--own=uk.gensokyo.CrashTestDummy1.*", + "--own=org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy1.*", + "--call=org.freedesktop.portal.*=*", + "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*", + "--log"}, + }, +} + +type dbusTestCase struct { + id string + c *hst.BusConfig + wantErr bool + wantErrF bool + bus [2]string + want []string +} + +var ( + testCasesExt = func() []dbusTestCase { + testCases := make([]dbusTestCase, len(samples)*2) + for i := range samples { + testCases[i] = samples[i] + + fi := &testCases[len(samples)+i] + *fi = samples[i] + + // create null-injected test cases + fi.wantErr = true + injectNulls := func(t *[]string) { + f := make([]string, len(*t)) + for i := range f { + f[i] = "\x00" + (*t)[i] + "\x00" + } + *t = f + } + + fi.c = new(hst.BusConfig) + *fi.c = *samples[i].c + injectNulls(&fi.c.See) + injectNulls(&fi.c.Talk) + injectNulls(&fi.c.Own) + } + return testCases + }() + + testCasePairs = func() map[string][2]dbusTestCase { + // enumerate test case pairs + var pc int + for _, tc := range samples { + if tc.id != "" { + pc++ + } + } + pairs := make(map[string][2]dbusTestCase, pc) + for i, tc := range testCasesExt { + if tc.id == "" { + continue + } + + // skip already enumerated system bus test + if tc.id[len(tc.id)-1] == '+' { + continue + } + + ftp := [2]dbusTestCase{tc} + + // system proxy tests always place directly after its user counterpart with id ending in + + if i+1 < len(testCasesExt) && testCasesExt[i+1].id[len(testCasesExt[i+1].id)-1] == '+' { + // attach system bus config + ftp[1] = testCasesExt[i+1] + + // check for misplaced/mismatching tests + if ftp[0].wantErr != ftp[1].wantErr || ftp[0].id+"+" != ftp[1].id { + panic("mismatching session/system pairing") + } + } + + k := tc.id + if tc.wantErr { + k = "malformed_" + k + } + pairs[k] = ftp + } + return pairs + }() +) diff --git a/internal/dbus/testdata/dev.vencord.Vesktop.json b/internal/dbus/testdata/dev.vencord.Vesktop.json new file mode 100644 index 00000000..05259774 --- /dev/null +++ b/internal/dbus/testdata/dev.vencord.Vesktop.json @@ -0,0 +1,18 @@ +{ + "talk":[ + "org.freedesktop.Notifications", + "org.kde.StatusNotifierWatcher" + ], + "own":[ + "dev.vencord.Vesktop.*", + "org.mpris.MediaPlayer2.dev.vencord.Vesktop.*" + ], + "call":{ + "org.freedesktop.portal.*":"*" + }, + "broadcast":{ + "org.freedesktop.portal.*":"@/org/freedesktop/portal/*" + }, + + "filter":true +} diff --git a/internal/dbus/testdata/org.chromium.Chromium+.json b/internal/dbus/testdata/org.chromium.Chromium+.json new file mode 100644 index 00000000..0a27d719 --- /dev/null +++ b/internal/dbus/testdata/org.chromium.Chromium+.json @@ -0,0 +1,9 @@ +{ + "talk":[ + "org.bluez", + "org.freedesktop.Avahi", + "org.freedesktop.UPower" + ], + + "filter":true +} \ No newline at end of file diff --git a/internal/dbus/testdata/org.chromium.Chromium.json b/internal/dbus/testdata/org.chromium.Chromium.json new file mode 100644 index 00000000..be027f39 --- /dev/null +++ b/internal/dbus/testdata/org.chromium.Chromium.json @@ -0,0 +1,24 @@ +{ + "talk":[ + "org.freedesktop.Notifications", + "org.freedesktop.FileManager1", + "org.freedesktop.ScreenSaver", + "org.freedesktop.secrets", + "org.kde.kwalletd5", + "org.kde.kwalletd6", + "org.gnome.SessionManager" + ], + "own":[ + "org.chromium.Chromium.*", + "org.mpris.MediaPlayer2.org.chromium.Chromium.*", + "org.mpris.MediaPlayer2.chromium.*" + ], + "call":{ + "org.freedesktop.portal.*":"*" + }, + "broadcast":{ + "org.freedesktop.portal.*":"@/org/freedesktop/portal/*" + }, + + "filter":true +} diff --git a/internal/dbus/testdata/uk.gensokyo.CrashTestDummy.json b/internal/dbus/testdata/uk.gensokyo.CrashTestDummy.json new file mode 100644 index 00000000..43272f67 --- /dev/null +++ b/internal/dbus/testdata/uk.gensokyo.CrashTestDummy.json @@ -0,0 +1,21 @@ +{ + "see": [ + "uk.gensokyo.CrashTestDummy1" + ], + "talk":[ + "org.freedesktop.Notifications" + ], + "own":[ + "uk.gensokyo.CrashTestDummy.*", + "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*" + ], + "call":{ + "org.freedesktop.portal.*":"*" + }, + "broadcast":{ + "org.freedesktop.portal.*":"@/org/freedesktop/portal/*" + }, + + "log": true, + "filter":true +} diff --git a/internal/outcome/dispatcher.go b/internal/outcome/dispatcher.go index 20c70a4b..a5339b20 100644 --- a/internal/outcome/dispatcher.go +++ b/internal/outcome/dispatcher.go @@ -14,8 +14,8 @@ import ( "hakurei.app/container/check" "hakurei.app/container/seccomp" "hakurei.app/container/std" + "hakurei.app/internal/dbus" "hakurei.app/internal/info" - "hakurei.app/internal/system/dbus" "hakurei.app/message" ) diff --git a/internal/outcome/outcome.go b/internal/outcome/outcome.go index b137d4f3..48f85a4b 100644 --- a/internal/outcome/outcome.go +++ b/internal/outcome/outcome.go @@ -9,9 +9,9 @@ import ( "hakurei.app/container" "hakurei.app/container/check" "hakurei.app/hst" + "hakurei.app/internal/acl" "hakurei.app/internal/env" "hakurei.app/internal/system" - "hakurei.app/internal/system/acl" "hakurei.app/message" ) diff --git a/internal/outcome/run_test.go b/internal/outcome/run_test.go index de2113f2..6908cd2f 100644 --- a/internal/outcome/run_test.go +++ b/internal/outcome/run_test.go @@ -21,9 +21,9 @@ 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/system/acl" - "hakurei.app/internal/system/dbus" "hakurei.app/message" ) diff --git a/internal/outcome/spcontainer.go b/internal/outcome/spcontainer.go index 738d3383..28697b41 100644 --- a/internal/outcome/spcontainer.go +++ b/internal/outcome/spcontainer.go @@ -16,9 +16,9 @@ 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/system/acl" - "hakurei.app/internal/system/dbus" "hakurei.app/internal/validate" "hakurei.app/message" ) diff --git a/internal/outcome/spcontainer_test.go b/internal/outcome/spcontainer_test.go index d6999b9d..51a04020 100644 --- a/internal/outcome/spcontainer_test.go +++ b/internal/outcome/spcontainer_test.go @@ -14,9 +14,9 @@ import ( "hakurei.app/container/std" "hakurei.app/container/stub" "hakurei.app/hst" + "hakurei.app/internal/acl" + "hakurei.app/internal/dbus" "hakurei.app/internal/system" - "hakurei.app/internal/system/acl" - "hakurei.app/internal/system/dbus" ) func TestSpParamsOp(t *testing.T) { diff --git a/internal/outcome/spdbus.go b/internal/outcome/spdbus.go index c123135e..8b3fbdbc 100644 --- a/internal/outcome/spdbus.go +++ b/internal/outcome/spdbus.go @@ -5,8 +5,8 @@ import ( "hakurei.app/container/fhs" "hakurei.app/hst" - "hakurei.app/internal/system/acl" - "hakurei.app/internal/system/dbus" + "hakurei.app/internal/acl" + "hakurei.app/internal/dbus" ) func init() { gob.Register(new(spDBusOp)) } diff --git a/internal/outcome/spdbus_test.go b/internal/outcome/spdbus_test.go index 2e489c22..350fc326 100644 --- a/internal/outcome/spdbus_test.go +++ b/internal/outcome/spdbus_test.go @@ -7,10 +7,10 @@ import ( "hakurei.app/container" "hakurei.app/container/stub" "hakurei.app/hst" + "hakurei.app/internal/acl" + "hakurei.app/internal/dbus" "hakurei.app/internal/helper" "hakurei.app/internal/system" - "hakurei.app/internal/system/acl" - "hakurei.app/internal/system/dbus" "hakurei.app/message" ) diff --git a/internal/outcome/sppulse_test.go b/internal/outcome/sppulse_test.go index e988ec08..4ec445db 100644 --- a/internal/outcome/sppulse_test.go +++ b/internal/outcome/sppulse_test.go @@ -11,8 +11,8 @@ import ( "hakurei.app/container/check" "hakurei.app/container/stub" "hakurei.app/hst" + "hakurei.app/internal/acl" "hakurei.app/internal/system" - "hakurei.app/internal/system/acl" ) func TestSpPulseOp(t *testing.T) { diff --git a/internal/outcome/spruntime.go b/internal/outcome/spruntime.go index 34c16341..1e066bd4 100644 --- a/internal/outcome/spruntime.go +++ b/internal/outcome/spruntime.go @@ -7,8 +7,8 @@ import ( "hakurei.app/container/fhs" "hakurei.app/container/std" "hakurei.app/hst" + "hakurei.app/internal/acl" "hakurei.app/internal/system" - "hakurei.app/internal/system/acl" ) const ( diff --git a/internal/outcome/spruntime_test.go b/internal/outcome/spruntime_test.go index b05cc1d9..18f3ce9c 100644 --- a/internal/outcome/spruntime_test.go +++ b/internal/outcome/spruntime_test.go @@ -8,8 +8,8 @@ import ( "hakurei.app/container/std" "hakurei.app/container/stub" "hakurei.app/hst" + "hakurei.app/internal/acl" "hakurei.app/internal/system" - "hakurei.app/internal/system/acl" ) func TestSpRuntimeOp(t *testing.T) { diff --git a/internal/outcome/sptmpdir.go b/internal/outcome/sptmpdir.go index dd675d67..9fb3f8d1 100644 --- a/internal/outcome/sptmpdir.go +++ b/internal/outcome/sptmpdir.go @@ -7,8 +7,8 @@ import ( "hakurei.app/container/fhs" "hakurei.app/container/std" "hakurei.app/hst" + "hakurei.app/internal/acl" "hakurei.app/internal/system" - "hakurei.app/internal/system/acl" ) func init() { gob.Register(spTmpdirOp{}) } diff --git a/internal/outcome/sptmpdir_test.go b/internal/outcome/sptmpdir_test.go index ccc8f3d3..61290c8d 100644 --- a/internal/outcome/sptmpdir_test.go +++ b/internal/outcome/sptmpdir_test.go @@ -8,8 +8,8 @@ import ( "hakurei.app/container/std" "hakurei.app/container/stub" "hakurei.app/hst" + "hakurei.app/internal/acl" "hakurei.app/internal/system" - "hakurei.app/internal/system/acl" ) func TestSpTmpdirOp(t *testing.T) { diff --git a/internal/outcome/spwayland.go b/internal/outcome/spwayland.go index a57c3c0a..7eccce2f 100644 --- a/internal/outcome/spwayland.go +++ b/internal/outcome/spwayland.go @@ -5,8 +5,8 @@ import ( "hakurei.app/container/check" "hakurei.app/hst" - "hakurei.app/internal/system/acl" - "hakurei.app/internal/system/wayland" + "hakurei.app/internal/acl" + "hakurei.app/internal/wayland" ) func init() { gob.Register(new(spWaylandOp)) } diff --git a/internal/outcome/spwayland_test.go b/internal/outcome/spwayland_test.go index 3c99abd7..828cc86c 100644 --- a/internal/outcome/spwayland_test.go +++ b/internal/outcome/spwayland_test.go @@ -6,9 +6,9 @@ import ( "hakurei.app/container" "hakurei.app/container/stub" "hakurei.app/hst" + "hakurei.app/internal/acl" "hakurei.app/internal/system" - "hakurei.app/internal/system/acl" - "hakurei.app/internal/system/wayland" + "hakurei.app/internal/wayland" ) func TestSpWaylandOp(t *testing.T) { diff --git a/internal/outcome/spx11.go b/internal/outcome/spx11.go index 8fb7ab4f..fd2b273a 100644 --- a/internal/outcome/spx11.go +++ b/internal/outcome/spx11.go @@ -11,7 +11,7 @@ import ( "hakurei.app/container/check" "hakurei.app/container/fhs" "hakurei.app/hst" - "hakurei.app/internal/system/acl" + "hakurei.app/internal/acl" ) var absX11SocketDir = fhs.AbsTmp.Append(".X11-unix") diff --git a/internal/outcome/spx11_test.go b/internal/outcome/spx11_test.go index 794da6ad..d4c10902 100644 --- a/internal/outcome/spx11_test.go +++ b/internal/outcome/spx11_test.go @@ -7,7 +7,7 @@ import ( "hakurei.app/container" "hakurei.app/container/stub" "hakurei.app/hst" - "hakurei.app/internal/system/acl" + "hakurei.app/internal/acl" ) func TestSpX11Op(t *testing.T) { diff --git a/internal/system/acl.go b/internal/system/acl.go index 854e812b..060d8854 100644 --- a/internal/system/acl.go +++ b/internal/system/acl.go @@ -8,7 +8,7 @@ import ( "hakurei.app/container/check" "hakurei.app/hst" - "hakurei.app/internal/system/acl" + "hakurei.app/internal/acl" ) // UpdatePerm calls UpdatePermType with the [Process] criteria. diff --git a/internal/system/acl/acl.go b/internal/system/acl/acl.go deleted file mode 100644 index 366ad533..00000000 --- a/internal/system/acl/acl.go +++ /dev/null @@ -1,33 +0,0 @@ -// Package acl implements simple ACL manipulation via libacl. -package acl - -/* -#cgo linux pkg-config: --static libacl - -#include "libacl-helper.h" -*/ -import "C" - -type Perm C.acl_perm_t - -const ( - Read Perm = C.ACL_READ - Write Perm = C.ACL_WRITE - Execute Perm = C.ACL_EXECUTE -) - -// Update replaces ACL_USER entry with qualifier uid. -func Update(name string, uid int, perms ...Perm) error { - var p *Perm - if len(perms) > 0 { - p = &perms[0] - } - - r, err := C.hakurei_acl_update_file_by_uid( - C.CString(name), - C.uid_t(uid), - (*C.acl_perm_t)(p), - C.size_t(len(perms)), - ) - return newAclPathError(name, int(r), err) -} diff --git a/internal/system/acl/acl_test.go b/internal/system/acl/acl_test.go deleted file mode 100644 index b0dd58d7..00000000 --- a/internal/system/acl/acl_test.go +++ /dev/null @@ -1,276 +0,0 @@ -package acl_test - -import ( - "bufio" - "bytes" - "errors" - "fmt" - "io" - "os" - "os/exec" - "path" - "reflect" - "strconv" - "testing" - - "hakurei.app/internal/system/acl" -) - -const testFileName = "acl.test" - -var ( - uid = os.Geteuid() - cred = int32(os.Geteuid()) -) - -func TestUpdate(t *testing.T) { - if os.Getenv("GO_TEST_SKIP_ACL") == "1" { - t.Log("acl test skipped") - t.SkipNow() - } - - testFilePath := path.Join(t.TempDir(), testFileName) - - if f, err := os.Create(testFilePath); err != nil { - t.Fatalf("Create: error = %v", err) - } else { - if err = f.Close(); err != nil { - t.Fatalf("Close: error = %v", err) - } - } - defer func() { - if err := os.Remove(testFilePath); err != nil { - t.Fatalf("Remove: error = %v", err) - } - }() - - cur := getfacl(t, testFilePath) - - t.Run("default entry count", func(t *testing.T) { - if len(cur) != 3 { - t.Fatalf("unexpected test file acl length %d", len(cur)) - } - }) - - t.Run("default clear mask", func(t *testing.T) { - if err := acl.Update(testFilePath, uid); err != nil { - t.Fatalf("Update: error = %v", err) - } - if cur = getfacl(t, testFilePath); len(cur) != 4 { - t.Fatalf("Update: %v", cur) - } - }) - - t.Run("default clear consistency", func(t *testing.T) { - if err := acl.Update(testFilePath, uid); err != nil { - t.Fatalf("Update: error = %v", err) - } - if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) { - t.Fatalf("Update: %v, want %v", val, cur) - } - }) - - testUpdate(t, testFilePath, "r--", cur, fAclPermRead, acl.Read) - testUpdate(t, testFilePath, "-w-", cur, fAclPermWrite, acl.Write) - testUpdate(t, testFilePath, "--x", cur, fAclPermExecute, acl.Execute) - testUpdate(t, testFilePath, "-wx", cur, fAclPermWrite|fAclPermExecute, acl.Write, acl.Execute) - testUpdate(t, testFilePath, "r-x", cur, fAclPermRead|fAclPermExecute, acl.Read, acl.Execute) - testUpdate(t, testFilePath, "rw-", cur, fAclPermRead|fAclPermWrite, acl.Read, acl.Write) - testUpdate(t, testFilePath, "rwx", cur, fAclPermRead|fAclPermWrite|fAclPermExecute, acl.Read, acl.Write, acl.Execute) -} - -func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val fAclPerm, perms ...acl.Perm) { - t.Run(name, func(t *testing.T) { - t.Cleanup(func() { - if err := acl.Update(testFilePath, uid); err != nil { - t.Fatalf("Update: error = %v", err) - } - if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) { - t.Fatalf("Update: %v, want %v", v, cur) - } - }) - - if err := acl.Update(testFilePath, uid, perms...); err != nil { - t.Fatalf("Update: error = %v", err) - } - r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred) - if r == nil { - t.Fatalf("Update did not add an ACL entry") - } - if !r.equals(fAclTypeUser, cred, val) { - t.Fatalf("Update(%s) = %s", name, r) - } - }) -} - -type ( - getFAclInvocation struct { - cmd *exec.Cmd - val []*getFAclResp - pe []error - } - - getFAclResp struct { - typ fAclType - cred int32 - val fAclPerm - - raw []byte - } - - fAclPerm uintptr - fAclType uint8 -) - -const fAclBufSize = 16 - -const ( - fAclPermRead fAclPerm = 1 << iota - fAclPermWrite - fAclPermExecute -) - -const ( - fAclTypeUser fAclType = iota - fAclTypeGroup - fAclTypeMask - fAclTypeOther -) - -func (c *getFAclInvocation) run(name string) error { - if c.cmd != nil { - panic("attempted to run twice") - } - - c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name) - - scanErr := make(chan error, 1) - if p, err := c.cmd.StdoutPipe(); err != nil { - return err - } else { - go c.parse(p, scanErr) - } - - if err := c.cmd.Start(); err != nil { - return err - } - - return errors.Join(<-scanErr, c.cmd.Wait()) -} - -func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) { - c.val = make([]*getFAclResp, 0, 4+fAclBufSize) - - s := bufio.NewScanner(pipe) - for s.Scan() { - fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3) - if len(fields) != 3 { - continue - } - - resp := getFAclResp{} - - switch string(fields[0]) { - case "user": - resp.typ = fAclTypeUser - case "group": - resp.typ = fAclTypeGroup - case "mask": - resp.typ = fAclTypeMask - case "other": - resp.typ = fAclTypeOther - default: - c.pe = append(c.pe, fmt.Errorf("unknown type %s", string(fields[0]))) - continue - } - - if len(fields[1]) == 0 { - resp.cred = -1 - } else { - if cred, err := strconv.Atoi(string(fields[1])); err != nil { - c.pe = append(c.pe, err) - continue - } else { - resp.cred = int32(cred) - if resp.cred < 0 { - c.pe = append(c.pe, fmt.Errorf("credential %d out of range", resp.cred)) - continue - } - } - } - - if len(fields[2]) != 3 { - c.pe = append(c.pe, fmt.Errorf("invalid perm length %d", len(fields[2]))) - continue - } else { - switch fields[2][0] { - case 'r': - resp.val |= fAclPermRead - case '-': - default: - c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0])) - continue - } - switch fields[2][1] { - case 'w': - resp.val |= fAclPermWrite - case '-': - default: - c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1])) - continue - } - switch fields[2][2] { - case 'x': - resp.val |= fAclPermExecute - case '-': - default: - c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2])) - continue - } - } - - resp.raw = make([]byte, len(s.Bytes())) - copy(resp.raw, s.Bytes()) - c.val = append(c.val, &resp) - } - scanErr <- s.Err() -} - -func (r *getFAclResp) String() string { - if r.raw != nil && len(r.raw) > 0 { - return string(r.raw) - } - - return "(user-initialised resp value)" -} - -func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool { - return r.typ == typ && r.cred == cred && r.val == val -} - -func getfacl(t *testing.T, name string) []*getFAclResp { - c := new(getFAclInvocation) - if err := c.run(name); err != nil { - t.Fatalf("getfacl: error = %v", err) - } - if len(c.pe) != 0 { - t.Errorf("errors encountered parsing getfacl output\n%s", errors.Join(c.pe...).Error()) - } - return c.val -} - -func respByCred(v []*getFAclResp, typ fAclType, cred int32) *getFAclResp { - j := -1 - for i, r := range v { - if r.typ == typ && r.cred == cred { - if j != -1 { - panic("invalid acl") - } - j = i - } - } - if j == -1 { - return nil - } - return v[j] -} diff --git a/internal/system/acl/libacl-helper.c b/internal/system/acl/libacl-helper.c deleted file mode 100644 index d1df8167..00000000 --- a/internal/system/acl/libacl-helper.c +++ /dev/null @@ -1,90 +0,0 @@ -#include "libacl-helper.h" -#include -#include -#include -#include - -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; -} diff --git a/internal/system/acl/libacl-helper.go b/internal/system/acl/libacl-helper.go deleted file mode 100644 index 22575296..00000000 --- a/internal/system/acl/libacl-helper.go +++ /dev/null @@ -1,40 +0,0 @@ -package acl - -import "os" - -func newAclPathError(name string, r int, err error) error { - pathError := &os.PathError{Path: name, Err: err} - switch r { - case 0: - return nil - - case -1: - pathError.Op = "acl_get_file" - case -2: - pathError.Op = "acl_get_tag_type" - case -3: - pathError.Op = "acl_get_qualifier" - case -4: - pathError.Op = "acl_delete_entry" - case -5: - pathError.Op = "acl_create_entry" - case -6: - pathError.Op = "acl_get_permset" - case -7: - pathError.Op = "acl_add_perm" - case -8: - pathError.Op = "acl_set_tag_type" - case -9: - pathError.Op = "acl_set_qualifier" - case -10: - pathError.Op = "acl_calc_mask" - case -11: - pathError.Op = "acl_valid" - case -12: - pathError.Op = "acl_set_file" - - default: // unreachable - pathError.Op = "setfacl" - } - return pathError -} diff --git a/internal/system/acl/libacl-helper.h b/internal/system/acl/libacl-helper.h deleted file mode 100644 index b86eb170..00000000 --- a/internal/system/acl/libacl-helper.h +++ /dev/null @@ -1,4 +0,0 @@ -#include - -int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid, - acl_perm_t *perms, size_t plen); diff --git a/internal/system/acl/libacl-helper_test.go b/internal/system/acl/libacl-helper_test.go deleted file mode 100644 index 13d23a3c..00000000 --- a/internal/system/acl/libacl-helper_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package acl - -import ( - "os" - "reflect" - "syscall" - "testing" - - "hakurei.app/container" -) - -func TestNewAclPathError(t *testing.T) { - testCases := []struct { - name string - path string - r int - err error - want error - }{ - {"nil", container.Nonexistent, 0, syscall.ENOTRECOVERABLE, nil}, - - {"acl_get_file", container.Nonexistent, -1, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_get_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_get_tag_type", container.Nonexistent, -2, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_get_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_get_qualifier", container.Nonexistent, -3, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_get_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_delete_entry", container.Nonexistent, -4, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_delete_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_create_entry", container.Nonexistent, -5, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_create_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_get_permset", container.Nonexistent, -6, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_get_permset", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_add_perm", container.Nonexistent, -7, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_add_perm", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_set_tag_type", container.Nonexistent, -8, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_set_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_set_qualifier", container.Nonexistent, -9, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_set_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_calc_mask", container.Nonexistent, -10, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_calc_mask", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_valid", container.Nonexistent, -11, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_valid", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_set_file", container.Nonexistent, -12, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_set_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - - {"acl", container.Nonexistent, -13, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "setfacl", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"invalid", container.Nonexistent, -0xdead, nil, - &os.PathError{Op: "setfacl", Path: container.Nonexistent}}, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := newAclPathError(tc.path, tc.r, tc.err) - if !reflect.DeepEqual(err, tc.want) { - t.Errorf("newAclPathError: %v, want %v", err, tc.want) - } - }) - } -} diff --git a/internal/system/acl/perms.go b/internal/system/acl/perms.go deleted file mode 100644 index fbc99e82..00000000 --- a/internal/system/acl/perms.go +++ /dev/null @@ -1,18 +0,0 @@ -package acl - -type Perms []Perm - -func (ps Perms) String() string { - var s = []byte("---") - for _, p := range ps { - switch p { - case Read: - s[0] = 'r' - case Write: - s[1] = 'w' - case Execute: - s[2] = 'x' - } - } - return string(s) -} diff --git a/internal/system/acl/perms_test.go b/internal/system/acl/perms_test.go deleted file mode 100644 index 3d5e3df5..00000000 --- a/internal/system/acl/perms_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package acl_test - -import ( - "testing" - - "hakurei.app/internal/system/acl" -) - -func TestPerms(t *testing.T) { - testCases := []struct { - name string - perms acl.Perms - }{ - {"---", acl.Perms{}}, - {"r--", acl.Perms{acl.Read}}, - {"-w-", acl.Perms{acl.Write}}, - {"--x", acl.Perms{acl.Execute}}, - {"rw-", acl.Perms{acl.Read, acl.Read, acl.Write}}, - {"r-x", acl.Perms{acl.Read, acl.Execute, acl.Execute}}, - {"-wx", acl.Perms{acl.Write, acl.Write, acl.Execute, acl.Execute}}, - {"rwx", acl.Perms{acl.Read, acl.Write, acl.Execute}}, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if got := tc.perms.String(); got != tc.name { - t.Errorf("String: %q, want %q", got, tc.name) - } - }) - } -} diff --git a/internal/system/acl_test.go b/internal/system/acl_test.go index 1fc3b1a2..f7829e24 100644 --- a/internal/system/acl_test.go +++ b/internal/system/acl_test.go @@ -7,7 +7,7 @@ import ( "hakurei.app/container/stub" "hakurei.app/hst" - "hakurei.app/internal/system/acl" + "hakurei.app/internal/acl" ) func TestACLUpdateOp(t *testing.T) { diff --git a/internal/system/dbus.go b/internal/system/dbus.go index b9021b81..8e2a7ec7 100644 --- a/internal/system/dbus.go +++ b/internal/system/dbus.go @@ -13,7 +13,7 @@ import ( "hakurei.app/container" "hakurei.app/hst" - "hakurei.app/internal/system/dbus" + "hakurei.app/internal/dbus" ) // ErrDBusConfig is returned when a required [hst.BusConfig] argument is nil. diff --git a/internal/system/dbus/address.go b/internal/system/dbus/address.go deleted file mode 100644 index edee7e20..00000000 --- a/internal/system/dbus/address.go +++ /dev/null @@ -1,193 +0,0 @@ -package dbus - -import ( - "bytes" - "encoding/hex" - "errors" - "fmt" - "slices" -) - -type AddrEntry struct { - Method string `json:"method"` - Values [][2]string `json:"values"` -} - -// EqualAddrEntries returns whether two slices of [AddrEntry] are equal. -func EqualAddrEntries(entries, target []AddrEntry) bool { - return slices.EqualFunc(entries, target, func(a AddrEntry, b AddrEntry) bool { - return a.Method == b.Method && slices.Equal(a.Values, b.Values) - }) -} - -// Parse parses D-Bus address according to -// https://dbus.freedesktop.org/doc/dbus-specification.html#addresses -func Parse(addr []byte) ([]AddrEntry, error) { - // Look for a semicolon - address := bytes.Split(bytes.TrimSuffix(addr, []byte{';'}), []byte{';'}) - - // Allocate for entries - v := make([]AddrEntry, len(address)) - - for i, s := range address { - var pairs [][]byte - - // Look for the colon : - if method, list, ok := bytes.Cut(s, []byte{':'}); !ok { - return v, &BadAddressError{ErrNoColon, i, s, -1, nil} - } else { - pairs = bytes.Split(list, []byte{','}) - v[i].Method = string(method) - v[i].Values = make([][2]string, len(pairs)) - } - - for j, pair := range pairs { - key, value, ok := bytes.Cut(pair, []byte{'='}) - if !ok { - return v, &BadAddressError{ErrBadPairSep, i, s, j, pair} - } - if len(key) == 0 { - return v, &BadAddressError{ErrBadPairKey, i, s, j, pair} - } - if len(value) == 0 { - return v, &BadAddressError{ErrBadPairVal, i, s, j, pair} - } - v[i].Values[j][0] = string(key) - - if val, errno := unescapeValue(value); errno != errSuccess { - return v, &BadAddressError{errno, i, s, j, pair} - } else { - v[i].Values[j][1] = string(val) - } - } - } - - return v, nil -} - -func unescapeValue(v []byte) (val []byte, errno ParseError) { - if l := len(v) - (bytes.Count(v, []byte{'%'}) * 2); l < 0 { - errno = ErrBadValLength - return - } else { - val = make([]byte, l) - } - - var i, skip int - for iu, b := range v { - if skip > 0 { - skip-- - continue - } - - if ib := bytes.IndexByte([]byte("-_/.\\*"), b); ib != -1 { // - // _/.\* - goto opt - } else if b >= '0' && b <= '9' { // 0-9 - goto opt - } else if b >= 'A' && b <= 'Z' { // A-Z - goto opt - } else if b >= 'a' && b <= 'z' { // a-z - goto opt - } - - if b != '%' { - errno = ErrBadValByte - break - } - - skip += 2 - if iu+2 >= len(v) { - errno = ErrBadValHexLength - break - } - if c, err := hex.Decode(val[i:i+1], v[iu+1:iu+3]); err != nil { - if errors.As(err, new(hex.InvalidByteError)) { - errno = ErrBadValHexByte - break - } - // unreachable - panic(err.Error()) - } else if c != 1 { - // unreachable - panic(fmt.Sprintf("invalid decode length %d", c)) - } - i++ - continue - - opt: - val[i] = b - i++ - } - - return -} - -type ParseError uint8 - -func (e ParseError) Error() string { - switch e { - case errSuccess: - panic("attempted to return success as error") - case ErrNoColon: - return "address does not contain a colon" - case ErrBadPairSep: - return "'=' character not found" - case ErrBadPairKey: - return "'=' character has no key preceding it" - case ErrBadPairVal: - return "'=' character has no value following it" - case ErrBadValLength: - return "unescaped value has impossible length" - case ErrBadValByte: - return "in D-Bus address, characters other than [-0-9A-Za-z_/.\\*] should have been escaped" - case ErrBadValHexLength: - return "in D-Bus address, percent character was not followed by two hex digits" - case ErrBadValHexByte: - return "in D-Bus address, percent character was followed by characters other than hex digits" - - default: - return fmt.Sprintf("parse error %d", e) - } -} - -const ( - errSuccess ParseError = iota - ErrNoColon - ErrBadPairSep - ErrBadPairKey - ErrBadPairVal - ErrBadValLength - ErrBadValByte - ErrBadValHexLength - ErrBadValHexByte -) - -type BadAddressError struct { - // error type - Type ParseError - - // bad entry position - EntryPos int - // bad entry value - EntryVal []byte - - // bad pair position - PairPos int - // bad pair value - PairVal []byte -} - -func (a *BadAddressError) Is(err error) bool { - var b *BadAddressError - return errors.As(err, &b) && a.Type == b.Type && - a.EntryPos == b.EntryPos && slices.Equal(a.EntryVal, b.EntryVal) && - a.PairPos == b.PairPos && slices.Equal(a.PairVal, b.PairVal) -} - -func (a *BadAddressError) Error() string { - return a.Type.Error() -} - -func (a *BadAddressError) Unwrap() error { - return a.Type -} diff --git a/internal/system/dbus/address_escape_test.go b/internal/system/dbus/address_escape_test.go deleted file mode 100644 index 4ef48f8b..00000000 --- a/internal/system/dbus/address_escape_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package dbus - -import ( - "testing" -) - -func TestUnescapeValue(t *testing.T) { - t.Parallel() - - testCases := []struct { - value string - want string - wantErr ParseError - }{ - // upstream test cases - {value: "abcde", want: "abcde"}, - {value: "", want: ""}, - {value: "%20%20", want: " "}, - {value: "%24", want: "$"}, - {value: "%25", want: "%"}, - {value: "abc%24", want: "abc$"}, - {value: "%24abc", want: "$abc"}, - {value: "abc%24abc", want: "abc$abc"}, - {value: "/", want: "/"}, - {value: "-", want: "-"}, - {value: "_", want: "_"}, - {value: "A", want: "A"}, - {value: "I", want: "I"}, - {value: "Z", want: "Z"}, - {value: "a", want: "a"}, - {value: "i", want: "i"}, - {value: "z", want: "z"}, - /* Bug: https://bugs.freedesktop.org/show_bug.cgi?id=53499 */ - {value: "%c3%b6", want: "\xc3\xb6"}, - - {value: "%a", wantErr: ErrBadValHexLength}, - {value: "%q", wantErr: ErrBadValHexLength}, - {value: "%az", wantErr: ErrBadValHexByte}, - {value: "%%", wantErr: ErrBadValLength}, - {value: "%$$", wantErr: ErrBadValHexByte}, - {value: "abc%a", wantErr: ErrBadValHexLength}, - {value: "%axyz", wantErr: ErrBadValHexByte}, - {value: "%", wantErr: ErrBadValLength}, - {value: "$", wantErr: ErrBadValByte}, - {value: " ", wantErr: ErrBadValByte}, - } - - for _, tc := range testCases { - t.Run("unescape "+tc.value, func(t *testing.T) { - t.Parallel() - - if got, errno := unescapeValue([]byte(tc.value)); errno != tc.wantErr { - t.Errorf("unescapeValue() errno = %v, wantErr %v", errno, tc.wantErr) - } else if tc.wantErr == errSuccess && string(got) != tc.want { - t.Errorf("unescapeValue() = %q, want %q", got, tc.want) - } - }) - } -} diff --git a/internal/system/dbus/address_test.go b/internal/system/dbus/address_test.go deleted file mode 100644 index 9ed48214..00000000 --- a/internal/system/dbus/address_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package dbus_test - -import ( - "errors" - "reflect" - "testing" - - "hakurei.app/internal/system/dbus" -) - -func TestParse(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - addr string - want []dbus.AddrEntry - wantErr error - }{ - { - name: "simple session unix", - addr: "unix:path=/run/user/1971/bus", - want: []dbus.AddrEntry{{ - Method: "unix", - Values: [][2]string{{"path", "/run/user/1971/bus"}}, - }}, - }, - { - name: "simple upper escape", - addr: "debug:name=Test,cat=cute,escaped=%c3%b6", - want: []dbus.AddrEntry{{ - Method: "debug", - Values: [][2]string{ - {"name", "Test"}, - {"cat", "cute"}, - {"escaped", "\xc3\xb6"}, - }, - }}, - }, - { - name: "simple bad escape", - addr: "debug:name=%", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadValLength, - EntryPos: 0, EntryVal: []byte("debug:name=%"), PairPos: 0, PairVal: []byte("name=%")}, - }, - - // upstream test cases - { - name: "full address success", - addr: "unix:path=/tmp/foo;debug:name=test,sliff=sloff;", - want: []dbus.AddrEntry{ - {Method: "unix", Values: [][2]string{{"path", "/tmp/foo"}}}, - {Method: "debug", Values: [][2]string{{"name", "test"}, {"sliff", "sloff"}}}, - }, - }, - { - name: "empty address", - addr: "", - wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon, - EntryVal: []byte{}, PairPos: -1}, - }, - { - name: "no body", - addr: "foo", - wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon, - EntryPos: 0, EntryVal: []byte("foo"), PairPos: -1}, - }, - { - name: "no pair separator", - addr: "foo:bar", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep, - EntryPos: 0, EntryVal: []byte("foo:bar"), PairPos: 0, PairVal: []byte("bar")}, - }, - { - name: "no pair separator multi pair", - addr: "foo:bar,baz", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep, - EntryPos: 0, EntryVal: []byte("foo:bar,baz"), PairPos: 0, PairVal: []byte("bar")}, - }, - { - name: "no pair separator single valid pair", - addr: "foo:bar=foo,baz", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep, - EntryPos: 0, EntryVal: []byte("foo:bar=foo,baz"), PairPos: 1, PairVal: []byte("baz")}, - }, - { - name: "no body single valid address", - addr: "foo:bar=foo;baz", - wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon, - EntryPos: 1, EntryVal: []byte("baz"), PairPos: -1}, - }, - { - name: "no key", - addr: "foo:=foo", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairKey, - EntryPos: 0, EntryVal: []byte("foo:=foo"), PairPos: 0, PairVal: []byte("=foo")}, - }, - { - name: "no value", - addr: "foo:foo=", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairVal, - EntryPos: 0, EntryVal: []byte("foo:foo="), PairPos: 0, PairVal: []byte("foo=")}, - }, - { - name: "no pair separator single valid pair trailing", - addr: "foo:foo,bar=baz", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep, - EntryPos: 0, EntryVal: []byte("foo:foo,bar=baz"), PairPos: 0, PairVal: []byte("foo")}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - if got, err := dbus.Parse([]byte(tc.addr)); !errors.Is(err, tc.wantErr) { - t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr) - } else if tc.wantErr == nil && !reflect.DeepEqual(got, tc.want) { - t.Errorf("Parse() = %#v, want %#v", got, tc.want) - } - }) - } -} diff --git a/internal/system/dbus/config.go b/internal/system/dbus/config.go deleted file mode 100644 index 2a09ee90..00000000 --- a/internal/system/dbus/config.go +++ /dev/null @@ -1,71 +0,0 @@ -package dbus - -import ( - "hakurei.app/hst" -) - -// ProxyPair is an upstream dbus address and a downstream socket path. -type ProxyPair [2]string - -// Args returns the xdg-dbus-proxy arguments equivalent of [hst.BusConfig]. -func Args(c *hst.BusConfig, bus ProxyPair) (args []string) { - argc := 2 + len(c.See) + len(c.Talk) + len(c.Own) + len(c.Call) + len(c.Broadcast) - if c.Log { - argc++ - } - if c.Filter { - argc++ - } - - args = make([]string, 0, argc) - args = append(args, bus[0], bus[1]) - if c.Filter { - args = append(args, "--filter") - } - for _, name := range c.See { - args = append(args, "--see="+name) - } - for _, name := range c.Talk { - args = append(args, "--talk="+name) - } - for _, name := range c.Own { - args = append(args, "--own="+name) - } - for name, rule := range c.Call { - args = append(args, "--call="+name+"="+rule) - } - for name, rule := range c.Broadcast { - args = append(args, "--broadcast="+name+"="+rule) - } - if c.Log { - args = append(args, "--log") - } - - return -} - -// NewConfig returns the address of a new [hst.BusConfig] with optional defaults. -func NewConfig(id string, defaults, mpris bool) *hst.BusConfig { - c := hst.BusConfig{ - Call: make(map[string]string), - Broadcast: make(map[string]string), - - Filter: true, - } - - if defaults { - c.Talk = []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"} - - c.Call["org.freedesktop.portal.*"] = "*" - c.Broadcast["org.freedesktop.portal.*"] = "@/org/freedesktop/portal/*" - - if id != "" { - c.Own = []string{id + ".*"} - if mpris { - c.Own = append(c.Own, "org.mpris.MediaPlayer2."+id+".*") - } - } - } - - return &c -} diff --git a/internal/system/dbus/config_test.go b/internal/system/dbus/config_test.go deleted file mode 100644 index dec4cb16..00000000 --- a/internal/system/dbus/config_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package dbus_test - -import ( - "reflect" - "slices" - "strings" - "testing" - - "hakurei.app/hst" - "hakurei.app/internal/system/dbus" -) - -func TestConfigArgs(t *testing.T) { - t.Parallel() - - for _, tc := range testCasesExt { - if tc.wantErr { - // args does not check for nulls - continue - } - - t.Run("build arguments for "+tc.id, func(t *testing.T) { - t.Parallel() - - if got := dbus.Args(tc.c, tc.bus); !slices.Equal(got, tc.want) { - t.Errorf("Args: %v, want %v", got, tc.want) - } - }) - } -} - -func TestNewConfig(t *testing.T) { - t.Parallel() - ids := [...]string{"org.chromium.Chromium", "dev.vencord.Vesktop"} - - type newTestCase struct { - id string - args [2]bool - want *hst.BusConfig - } - - // populate tests from IDs in generic tests - tcs := make([]newTestCase, 0, (len(ids)+1)*4) - // tests for defaults without id - tcs = append(tcs, - newTestCase{"", [2]bool{false, false}, &hst.BusConfig{ - Call: make(map[string]string), - Broadcast: make(map[string]string), - Filter: true, - }}, - newTestCase{"", [2]bool{false, true}, &hst.BusConfig{ - Call: make(map[string]string), - Broadcast: make(map[string]string), - Filter: true, - }}, - newTestCase{"", [2]bool{true, false}, &hst.BusConfig{ - Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Filter: true, - }}, - newTestCase{"", [2]bool{true, true}, &hst.BusConfig{ - Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Filter: true, - }}, - ) - for _, id := range ids { - tcs = append(tcs, - newTestCase{id, [2]bool{false, false}, &hst.BusConfig{ - Call: make(map[string]string), - Broadcast: make(map[string]string), - Filter: true, - }}, - newTestCase{id, [2]bool{false, true}, &hst.BusConfig{ - Call: make(map[string]string), - Broadcast: make(map[string]string), - Filter: true, - }}, - newTestCase{id, [2]bool{true, false}, &hst.BusConfig{ - Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}, - Own: []string{id + ".*"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Filter: true, - }}, - newTestCase{id, [2]bool{true, true}, &hst.BusConfig{ - Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}, - Own: []string{id + ".*", "org.mpris.MediaPlayer2." + id + ".*"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Filter: true, - }}, - ) - } - - for _, tc := range tcs { - name := new(strings.Builder) - name.WriteString("create new configuration struct") - - if tc.args[0] { - name.WriteString(" with builtin defaults") - if tc.args[1] { - name.WriteString(" (mpris)") - } - } - - if tc.id != "" { - name.WriteString(" for application ID ") - name.WriteString(tc.id) - } - - t.Run(name.String(), func(t *testing.T) { - t.Parallel() - - if gotC := dbus.NewConfig(tc.id, tc.args[0], tc.args[1]); !reflect.DeepEqual(gotC, tc.want) { - t.Errorf("NewConfig(%q, %t, %t) = %v, want %v", - tc.id, tc.args[0], tc.args[1], - gotC, tc.want) - } - }) - } -} diff --git a/internal/system/dbus/dbus.go b/internal/system/dbus/dbus.go deleted file mode 100644 index 9ea89f51..00000000 --- a/internal/system/dbus/dbus.go +++ /dev/null @@ -1,69 +0,0 @@ -// Package dbus wraps xdg-dbus-proxy and implements configuration and sandboxing of the underlying helper process. -package dbus - -import ( - "fmt" - "os" - "sync" -) - -const ( - /* - SessionBusAddress is the name of the environment variable where the address of the login session message bus is given in. - - If that variable is not set, applications may also try to read the address from the X Window System root window property _DBUS_SESSION_BUS_ADDRESS. - The root window property must have type STRING. The environment variable should have precedence over the root window property. - - The address of the login session message bus is given in the DBUS_SESSION_BUS_ADDRESS environment variable. - If DBUS_SESSION_BUS_ADDRESS is not set, or if it's set to the string "autolaunch:", - the system should use platform-specific methods of locating a running D-Bus session server, - or starting one if a running instance cannot be found. - Note that this mechanism is not recommended for attempting to determine if a daemon is running. - It is inherently racy to attempt to make this determination, since the bus daemon may be started just before or just after the determination is made. - Therefore, it is recommended that applications do not try to make this determination for their functionality purposes, and instead they should attempt to start the server. - - This package diverges from the specification, as the caller is unlikely to be an X client, or be in a position to autolaunch a dbus server. - So a fallback address with a socket located in the well-known default XDG_RUNTIME_DIR formatting is used. - */ - SessionBusAddress = "DBUS_SESSION_BUS_ADDRESS" - - /* - SystemBusAddress is the name of the environment variable where the address of the system message bus is given in. - - If that variable is not set, applications should try to connect to the well-known address unix:path=/var/run/dbus/system_bus_socket. - Implementations of the well-known system bus should listen on an address that will result in that connection being successful. - */ - SystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS" - - // FallbackSystemBusAddress is used when [SystemBusAddress] is not set. - FallbackSystemBusAddress = "unix:path=/var/run/dbus/system_bus_socket" -) - -var ( - address [2]string - addressOnce sync.Once -) - -// Address returns the session and system bus addresses copied from environment, -// or appropriate fallback values if they are not set. -func Address() (session, system string) { - addressOnce.Do(func() { - // resolve upstream session bus address - if addr, ok := os.LookupEnv(SessionBusAddress); !ok { - // fall back to default format - address[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid()) - } else { - address[0] = addr - } - - // resolve upstream system bus address - if addr, ok := os.LookupEnv(SystemBusAddress); !ok { - // fall back to default hardcoded value - address[1] = FallbackSystemBusAddress - } else { - address[1] = addr - } - }) - - return address[0], address[1] -} diff --git a/internal/system/dbus/dbus_test.go b/internal/system/dbus/dbus_test.go deleted file mode 100644 index 9fc2b539..00000000 --- a/internal/system/dbus/dbus_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package dbus_test - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "strings" - "syscall" - "testing" - "time" - - "hakurei.app/internal/helper" - "hakurei.app/internal/system/dbus" - "hakurei.app/message" -) - -func TestFinalise(t *testing.T) { - if _, err := dbus.Finalise(dbus.ProxyPair{}, dbus.ProxyPair{}, nil, nil); !errors.Is(err, syscall.EBADE) { - t.Errorf("Finalise: error = %v, want %v", - err, syscall.EBADE) - } - - for id, tc := range testCasePairs { - t.Run("create final for "+id, func(t *testing.T) { - var wt io.WriterTo - if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr { - t.Errorf("Finalise: error = %v, wantErr %v", - err, tc[0].wantErr) - return - } else { - wt = v - } - - // rest of the tests happen for sealed instances - if tc[0].wantErr { - return - } - - // build null-terminated string from wanted args - want := new(strings.Builder) - args := append(tc[0].want, tc[1].want...) - for _, arg := range args { - want.WriteString(arg) - want.WriteByte(0) - } - - got := new(strings.Builder) - if _, err := wt.WriteTo(got); err != nil { - t.Errorf("WriteTo: error = %v", err) - } - - if want.String() != got.String() { - t.Errorf("Seal: %q, want %q", - got.String(), want.String()) - } - }) - } -} - -func TestProxyStartWaitCloseString(t *testing.T) { - t.Run("sandbox", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, true) }) - t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) }) -} - -const ( - stubProxyTimeout = 5 * time.Second -) - -func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { - { - oldWaitDelay := helper.WaitDelay - helper.WaitDelay = 16 * time.Second - t.Cleanup(func() { helper.WaitDelay = oldWaitDelay }) - } - - { - proxyName := dbus.ProxyName - dbus.ProxyName = os.Args[0] - t.Cleanup(func() { dbus.ProxyName = proxyName }) - } - - var p *dbus.Proxy - - t.Run("string for nil proxy", func(t *testing.T) { - want := "(invalid dbus proxy)" - if got := p.String(); got != want { - t.Errorf("String: %q, want %q", - got, want) - } - }) - - t.Run("invalid start", func(t *testing.T) { - if !useSandbox { - p = dbus.NewDirect(t.Context(), message.New(nil), nil, nil) - } else { - p = dbus.New(t.Context(), message.New(nil), nil, nil) - } - - if err := p.Start(); !errors.Is(err, syscall.ENOTRECOVERABLE) { - t.Errorf("Start: error = %q, wantErr %q", err, syscall.ENOTRECOVERABLE) - return - } - }) - - for id, tc := range testCasePairs { - // this test does not test errors - if tc[0].wantErr { - continue - } - - t.Run("proxy for "+id, func(t *testing.T) { - var final *dbus.Final - t.Run("finalise", func(t *testing.T) { - if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); err != nil { - t.Errorf("Finalise: error = %v, wantErr %v", err, tc[0].wantErr) - return - } else { - final = v - } - }) - - ctx, cancel := context.WithTimeout(t.Context(), stubProxyTimeout) - defer cancel() - output := new(strings.Builder) - if !useSandbox { - p = dbus.NewDirect(ctx, message.New(nil), final, output) - } else { - p = dbus.New(ctx, message.New(nil), final, output) - } - - { // check invalid wait behaviour - wantErr := "dbus: not started" - if err := p.Wait(); err == nil || err.Error() != wantErr { - t.Errorf("Wait: error = %v, wantErr %v", err, wantErr) - } - } - - { // check string behaviour - want := "(unused dbus proxy)" - if got := p.String(); got != want { - t.Errorf("String: %q, want %q", got, want) - return - } - } - - if err := p.Start(); err != nil { - t.Fatalf("Start: error = %v", err) - } - - { // check running string behaviour - wantSubstr := fmt.Sprintf("%s --args=3 --fd=4", os.Args[0]) - if useSandbox { - wantSubstr = `argv: ["xdg-dbus-proxy" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf` - } - if got := p.String(); !strings.Contains(got, wantSubstr) { - t.Errorf("String: %q, want %q", - got, wantSubstr) - return - } - } - - p.Close() - if err := p.Wait(); err != nil { - t.Errorf("Wait: error = %v\noutput: %s", err, output.String()) - } - }) - } -} diff --git a/internal/system/dbus/export_test.go b/internal/system/dbus/export_test.go deleted file mode 100644 index bc24f3f3..00000000 --- a/internal/system/dbus/export_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package dbus - -import ( - "context" - "io" - - "hakurei.app/message" -) - -// NewDirect returns a new instance of [Proxy] with its sandbox disabled. -func NewDirect(ctx context.Context, msg message.Msg, final *Final, output io.Writer) *Proxy { - p := New(ctx, msg, final, output) - p.useSandbox = false - return p -} diff --git a/internal/system/dbus/proc.go b/internal/system/dbus/proc.go deleted file mode 100644 index c934e89e..00000000 --- a/internal/system/dbus/proc.go +++ /dev/null @@ -1,188 +0,0 @@ -package dbus - -import ( - "context" - "errors" - "os" - "os/exec" - "strconv" - "syscall" - - "hakurei.app/container" - "hakurei.app/container/check" - "hakurei.app/container/seccomp" - "hakurei.app/container/std" - "hakurei.app/internal/helper" - "hakurei.app/ldd" -) - -// Start starts and configures a D-Bus proxy process. -func (p *Proxy) Start() error { - if p.final == nil || p.final.WriterTo == nil { - return syscall.ENOTRECOVERABLE - } - - p.mu.Lock() - defer p.mu.Unlock() - p.pmu.Lock() - defer p.pmu.Unlock() - - if p.cancel != nil || p.cause != nil { - return errors.New("dbus: already started") - } - - ctx, cancel := context.WithCancelCause(p.ctx) - - if !p.useSandbox { - p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) { - if p.output != nil { - cmd.Stdout, cmd.Stderr = p.output, p.output - } - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - cmd.Env = make([]string, 0) - }, nil) - } else { - var toolPath *check.Absolute - if a, err := check.NewAbs(p.name); err != nil { - if p.name, err = exec.LookPath(p.name); err != nil { - return err - } else if toolPath, err = check.NewAbs(p.name); err != nil { - return err - } - } else { - toolPath = a - } - - var libPaths []*check.Absolute - if entries, err := ldd.Resolve(ctx, p.msg, toolPath); err != nil { - return err - } else { - libPaths = ldd.Path(entries) - } - - p.helper = helper.New( - ctx, p.msg, toolPath, "xdg-dbus-proxy", - p.final, true, - argF, func(z *container.Container) { - z.SeccompFlags |= seccomp.AllowMultiarch - z.SeccompPresets |= std.PresetStrict - z.Hostname = "hakurei-dbus" - if p.output != nil { - z.Stdout, z.Stderr = p.output, p.output - } - - // these lib paths are unpredictable, so mount them first so they cannot cover anything - for _, name := range libPaths { - z.Bind(name, name, 0) - } - - // upstream bus directories - upstreamPaths := make([]*check.Absolute, 0, 2) - for _, addr := range [][]AddrEntry{p.final.SessionUpstream, p.final.SystemUpstream} { - for _, ent := range addr { - if ent.Method != "unix" { - continue - } - for _, pair := range ent.Values { - if pair[0] != "path" { - continue - } - if a, err := check.NewAbs(pair[1]); err != nil { - continue - } else { - upstreamPaths = append(upstreamPaths, a.Dir()) - } - } - } - } - check.SortAbs(upstreamPaths) - upstreamPaths = check.CompactAbs(upstreamPaths) - for _, name := range upstreamPaths { - z.Bind(name, name, 0) - } - z.HostNet = len(upstreamPaths) == 0 - z.HostAbstract = z.HostNet - - // parent directories of bind paths - sockDirPaths := make([]*check.Absolute, 0, 2) - if a, err := check.NewAbs(p.final.Session[1]); err == nil { - sockDirPaths = append(sockDirPaths, a.Dir()) - } - if a, err := check.NewAbs(p.final.System[1]); err == nil { - sockDirPaths = append(sockDirPaths, a.Dir()) - } - check.SortAbs(sockDirPaths) - sockDirPaths = check.CompactAbs(sockDirPaths) - for _, name := range sockDirPaths { - z.Bind(name, name, std.BindWritable) - } - - // xdg-dbus-proxy bin path - binPath := toolPath.Dir() - z.Bind(binPath, binPath, 0) - }, nil) - } - - if err := p.helper.Start(); err != nil { - cancel(err) - p.helper = nil - return err - } - - p.cancel, p.cause = cancel, func() error { return context.Cause(ctx) } - return nil -} - -var proxyClosed = errors.New("proxy closed") - -// Wait blocks until xdg-dbus-proxy exits and releases resources. -func (p *Proxy) Wait() error { - p.mu.RLock() - defer p.mu.RUnlock() - - p.pmu.RLock() - if p.helper == nil || p.cancel == nil || p.cause == nil { - p.pmu.RUnlock() - return errors.New("dbus: not started") - } - - var errs [3]error - - errs[0] = p.helper.Wait() - if errors.Is(errs[0], context.Canceled) && - errors.Is(p.cause(), proxyClosed) { - errs[0] = nil - } - p.pmu.RUnlock() - - // ensure socket removal so ephemeral directory is empty at revert - if err := os.Remove(p.final.Session[1]); err != nil && !errors.Is(err, os.ErrNotExist) { - errs[1] = err - } - if p.final.System[1] != "" { - if err := os.Remove(p.final.System[1]); err != nil && !errors.Is(err, os.ErrNotExist) { - errs[2] = err - } - } - - return errors.Join(errs[:]...) -} - -// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy. -func (p *Proxy) Close() { - p.pmu.Lock() - defer p.pmu.Unlock() - - if p.cancel == nil { - panic("dbus: not started") - } - p.cancel(proxyClosed) -} - -func argF(argsFd, statFd int) []string { - if statFd == -1 { - return []string{"--args=" + strconv.Itoa(argsFd)} - } else { - return []string{"--args=" + strconv.Itoa(argsFd), "--fd=" + strconv.Itoa(statFd)} - } -} diff --git a/internal/system/dbus/proc_test.go b/internal/system/dbus/proc_test.go deleted file mode 100644 index 544856ed..00000000 --- a/internal/system/dbus/proc_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package dbus_test - -import ( - "os" - "testing" - - "hakurei.app/container" - "hakurei.app/internal/helper" -) - -func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) } diff --git a/internal/system/dbus/proxy.go b/internal/system/dbus/proxy.go deleted file mode 100644 index 9de49c41..00000000 --- a/internal/system/dbus/proxy.go +++ /dev/null @@ -1,105 +0,0 @@ -package dbus - -import ( - "context" - "io" - "sync" - "syscall" - - "hakurei.app/hst" - "hakurei.app/internal/helper" - "hakurei.app/message" -) - -// ProxyName is the file name or path to the proxy program. -// Overriding ProxyName will only affect Proxy instance created after the change. -var ProxyName = "xdg-dbus-proxy" - -// Proxy holds the state of a xdg-dbus-proxy process, and should never be copied. -type Proxy struct { - helper helper.Helper - ctx context.Context - msg message.Msg - - cancel context.CancelCauseFunc - cause func() error - - final *Final - output io.Writer - useSandbox bool - - name string - - mu, pmu sync.RWMutex -} - -func (p *Proxy) String() string { - if p == nil { - return "(invalid dbus proxy)" - } - - p.mu.RLock() - defer p.mu.RUnlock() - - if p.helper != nil { - return p.helper.String() - } - - return "(unused dbus proxy)" -} - -// Final describes the outcome of a proxy configuration. -type Final struct { - Session, System ProxyPair - // parsed upstream address - SessionUpstream, SystemUpstream []AddrEntry - io.WriterTo -} - -// Finalise creates a checked argument writer for [Proxy]. -func Finalise(sessionBus, systemBus ProxyPair, session, system *hst.BusConfig) (final *Final, err error) { - if session == nil && system == nil { - return nil, syscall.EBADE - } - - var args []string - if session != nil { - if err = session.CheckInterfaces("session"); err != nil { - return - } - args = append(args, Args(session, sessionBus)...) - } - if system != nil { - if err = system.CheckInterfaces("system"); err != nil { - return - } - args = append(args, Args(system, systemBus)...) - } - - final = &Final{Session: sessionBus, System: systemBus} - - final.WriterTo, err = helper.NewCheckedArgs(args...) - if err != nil { - return - } - - if session != nil { - final.SessionUpstream, err = Parse([]byte(final.Session[0])) - if err != nil { - return - } - } - if system != nil { - final.SystemUpstream, err = Parse([]byte(final.System[0])) - if err != nil { - return - } - } - - return -} - -// New returns a new instance of [Proxy]. -func New(ctx context.Context, msg message.Msg, final *Final, output io.Writer) *Proxy { - return &Proxy{name: ProxyName, ctx: ctx, msg: msg, final: final, output: output, useSandbox: true} -} diff --git a/internal/system/dbus/samples_test.go b/internal/system/dbus/samples_test.go deleted file mode 100644 index 7db3d6b9..00000000 --- a/internal/system/dbus/samples_test.go +++ /dev/null @@ -1,214 +0,0 @@ -package dbus_test - -import ( - "hakurei.app/hst" -) - -const ( - sampleHostPath = "/tmp/bus" - sampleHostAddr = "unix:path=" + sampleHostPath - sampleBindPath = "/tmp/proxied_bus" -) - -var samples = []dbusTestCase{ - { - "org.chromium.Chromium", &hst.BusConfig{ - See: nil, - Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver", - "org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"}, - Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*", - "org.mpris.MediaPlayer2.chromium.*"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Log: false, - Filter: true, - }, false, false, - [2]string{sampleHostAddr, sampleBindPath}, - []string{ - sampleHostAddr, - sampleBindPath, - "--filter", - "--talk=org.freedesktop.Notifications", - "--talk=org.freedesktop.FileManager1", - "--talk=org.freedesktop.ScreenSaver", - "--talk=org.freedesktop.secrets", - "--talk=org.kde.kwalletd5", - "--talk=org.kde.kwalletd6", - "--talk=org.gnome.SessionManager", - "--own=org.chromium.Chromium.*", - "--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*", - "--own=org.mpris.MediaPlayer2.chromium.*", - "--call=org.freedesktop.portal.*=*", - "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*", - }, - }, - { - "org.chromium.Chromium+", &hst.BusConfig{ - See: nil, - Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"}, - Own: nil, - Call: nil, - Broadcast: nil, - Log: false, - Filter: true, - }, false, false, - [2]string{sampleHostAddr, sampleBindPath}, - []string{ - sampleHostAddr, - sampleBindPath, - "--filter", - "--talk=org.bluez", - "--talk=org.freedesktop.Avahi", - "--talk=org.freedesktop.UPower", - }, - }, - - { - "dev.vencord.Vesktop", &hst.BusConfig{ - See: nil, - Talk: []string{"org.freedesktop.Notifications", "org.kde.StatusNotifierWatcher"}, - Own: []string{"dev.vencord.Vesktop.*", "org.mpris.MediaPlayer2.dev.vencord.Vesktop.*"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Log: false, - Filter: true, - }, false, false, - [2]string{sampleHostAddr, sampleBindPath}, - []string{ - sampleHostAddr, - sampleBindPath, - "--filter", - "--talk=org.freedesktop.Notifications", - "--talk=org.kde.StatusNotifierWatcher", - "--own=dev.vencord.Vesktop.*", - "--own=org.mpris.MediaPlayer2.dev.vencord.Vesktop.*", - "--call=org.freedesktop.portal.*=*", - "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*"}, - }, - - { - "uk.gensokyo.CrashTestDummy", &hst.BusConfig{ - See: []string{"uk.gensokyo.CrashTestDummy1"}, - Talk: []string{"org.freedesktop.Notifications"}, - Own: []string{"uk.gensokyo.CrashTestDummy.*", "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Log: true, - Filter: true, - }, false, false, - [2]string{sampleHostAddr, sampleBindPath}, - []string{ - sampleHostAddr, - sampleBindPath, - "--filter", - "--see=uk.gensokyo.CrashTestDummy1", - "--talk=org.freedesktop.Notifications", - "--own=uk.gensokyo.CrashTestDummy.*", - "--own=org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*", - "--call=org.freedesktop.portal.*=*", - "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*", - "--log"}, - }, - { - "uk.gensokyo.CrashTestDummy1", &hst.BusConfig{ - See: []string{"uk.gensokyo.CrashTestDummy"}, - Talk: []string{"org.freedesktop.Notifications"}, - Own: []string{"uk.gensokyo.CrashTestDummy1.*", "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy1.*"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Log: true, - Filter: true, - }, false, true, - [2]string{sampleHostAddr, sampleBindPath}, - []string{ - sampleHostAddr, - sampleBindPath, - "--filter", - "--see=uk.gensokyo.CrashTestDummy", - "--talk=org.freedesktop.Notifications", - "--own=uk.gensokyo.CrashTestDummy1.*", - "--own=org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy1.*", - "--call=org.freedesktop.portal.*=*", - "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*", - "--log"}, - }, -} - -type dbusTestCase struct { - id string - c *hst.BusConfig - wantErr bool - wantErrF bool - bus [2]string - want []string -} - -var ( - testCasesExt = func() []dbusTestCase { - testCases := make([]dbusTestCase, len(samples)*2) - for i := range samples { - testCases[i] = samples[i] - - fi := &testCases[len(samples)+i] - *fi = samples[i] - - // create null-injected test cases - fi.wantErr = true - injectNulls := func(t *[]string) { - f := make([]string, len(*t)) - for i := range f { - f[i] = "\x00" + (*t)[i] + "\x00" - } - *t = f - } - - fi.c = new(hst.BusConfig) - *fi.c = *samples[i].c - injectNulls(&fi.c.See) - injectNulls(&fi.c.Talk) - injectNulls(&fi.c.Own) - } - return testCases - }() - - testCasePairs = func() map[string][2]dbusTestCase { - // enumerate test case pairs - var pc int - for _, tc := range samples { - if tc.id != "" { - pc++ - } - } - pairs := make(map[string][2]dbusTestCase, pc) - for i, tc := range testCasesExt { - if tc.id == "" { - continue - } - - // skip already enumerated system bus test - if tc.id[len(tc.id)-1] == '+' { - continue - } - - ftp := [2]dbusTestCase{tc} - - // system proxy tests always place directly after its user counterpart with id ending in + - if i+1 < len(testCasesExt) && testCasesExt[i+1].id[len(testCasesExt[i+1].id)-1] == '+' { - // attach system bus config - ftp[1] = testCasesExt[i+1] - - // check for misplaced/mismatching tests - if ftp[0].wantErr != ftp[1].wantErr || ftp[0].id+"+" != ftp[1].id { - panic("mismatching session/system pairing") - } - } - - k := tc.id - if tc.wantErr { - k = "malformed_" + k - } - pairs[k] = ftp - } - return pairs - }() -) diff --git a/internal/system/dbus/testdata/dev.vencord.Vesktop.json b/internal/system/dbus/testdata/dev.vencord.Vesktop.json deleted file mode 100644 index 05259774..00000000 --- a/internal/system/dbus/testdata/dev.vencord.Vesktop.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "talk":[ - "org.freedesktop.Notifications", - "org.kde.StatusNotifierWatcher" - ], - "own":[ - "dev.vencord.Vesktop.*", - "org.mpris.MediaPlayer2.dev.vencord.Vesktop.*" - ], - "call":{ - "org.freedesktop.portal.*":"*" - }, - "broadcast":{ - "org.freedesktop.portal.*":"@/org/freedesktop/portal/*" - }, - - "filter":true -} diff --git a/internal/system/dbus/testdata/org.chromium.Chromium+.json b/internal/system/dbus/testdata/org.chromium.Chromium+.json deleted file mode 100644 index 0a27d719..00000000 --- a/internal/system/dbus/testdata/org.chromium.Chromium+.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "talk":[ - "org.bluez", - "org.freedesktop.Avahi", - "org.freedesktop.UPower" - ], - - "filter":true -} \ No newline at end of file diff --git a/internal/system/dbus/testdata/org.chromium.Chromium.json b/internal/system/dbus/testdata/org.chromium.Chromium.json deleted file mode 100644 index be027f39..00000000 --- a/internal/system/dbus/testdata/org.chromium.Chromium.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "talk":[ - "org.freedesktop.Notifications", - "org.freedesktop.FileManager1", - "org.freedesktop.ScreenSaver", - "org.freedesktop.secrets", - "org.kde.kwalletd5", - "org.kde.kwalletd6", - "org.gnome.SessionManager" - ], - "own":[ - "org.chromium.Chromium.*", - "org.mpris.MediaPlayer2.org.chromium.Chromium.*", - "org.mpris.MediaPlayer2.chromium.*" - ], - "call":{ - "org.freedesktop.portal.*":"*" - }, - "broadcast":{ - "org.freedesktop.portal.*":"@/org/freedesktop/portal/*" - }, - - "filter":true -} diff --git a/internal/system/dbus/testdata/uk.gensokyo.CrashTestDummy.json b/internal/system/dbus/testdata/uk.gensokyo.CrashTestDummy.json deleted file mode 100644 index 43272f67..00000000 --- a/internal/system/dbus/testdata/uk.gensokyo.CrashTestDummy.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "see": [ - "uk.gensokyo.CrashTestDummy1" - ], - "talk":[ - "org.freedesktop.Notifications" - ], - "own":[ - "uk.gensokyo.CrashTestDummy.*", - "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*" - ], - "call":{ - "org.freedesktop.portal.*":"*" - }, - "broadcast":{ - "org.freedesktop.portal.*":"@/org/freedesktop/portal/*" - }, - - "log": true, - "filter":true -} diff --git a/internal/system/dbus_test.go b/internal/system/dbus_test.go index f79c8482..10d242ca 100644 --- a/internal/system/dbus_test.go +++ b/internal/system/dbus_test.go @@ -11,8 +11,8 @@ import ( "hakurei.app/container/stub" "hakurei.app/hst" + "hakurei.app/internal/dbus" "hakurei.app/internal/helper" - "hakurei.app/internal/system/dbus" ) func TestDBusProxyOp(t *testing.T) { diff --git a/internal/system/dispatcher.go b/internal/system/dispatcher.go index 86aebcaa..1ea9f108 100644 --- a/internal/system/dispatcher.go +++ b/internal/system/dispatcher.go @@ -7,9 +7,9 @@ import ( "os" "hakurei.app/hst" - "hakurei.app/internal/system/acl" - "hakurei.app/internal/system/dbus" - "hakurei.app/internal/system/xcb" + "hakurei.app/internal/acl" + "hakurei.app/internal/dbus" + "hakurei.app/internal/xcb" ) type osFile interface { diff --git a/internal/system/dispatcher_test.go b/internal/system/dispatcher_test.go index 8000f0c7..95aab3e4 100644 --- a/internal/system/dispatcher_test.go +++ b/internal/system/dispatcher_test.go @@ -10,9 +10,9 @@ import ( "hakurei.app/container/stub" "hakurei.app/hst" - "hakurei.app/internal/system/acl" - "hakurei.app/internal/system/dbus" - "hakurei.app/internal/system/xcb" + "hakurei.app/internal/acl" + "hakurei.app/internal/dbus" + "hakurei.app/internal/xcb" ) // call initialises a [stub.Call]. diff --git a/internal/system/system_test.go b/internal/system/system_test.go index b0773b0f..f36ff54e 100644 --- a/internal/system/system_test.go +++ b/internal/system/system_test.go @@ -11,7 +11,7 @@ import ( "hakurei.app/container/check" "hakurei.app/container/stub" "hakurei.app/hst" - "hakurei.app/internal/system/xcb" + "hakurei.app/internal/xcb" "hakurei.app/message" ) diff --git a/internal/system/wayland.go b/internal/system/wayland.go index 8883148e..c423fe24 100644 --- a/internal/system/wayland.go +++ b/internal/system/wayland.go @@ -7,8 +7,8 @@ import ( "hakurei.app/container/check" "hakurei.app/hst" - "hakurei.app/internal/system/acl" - "hakurei.app/internal/system/wayland" + "hakurei.app/internal/acl" + "hakurei.app/internal/wayland" ) type waylandConn interface { diff --git a/internal/system/wayland/conn.go b/internal/system/wayland/conn.go deleted file mode 100644 index c55fe704..00000000 --- a/internal/system/wayland/conn.go +++ /dev/null @@ -1,122 +0,0 @@ -package wayland - -import ( - "errors" - "net" - "os" - "runtime" - "sync" - "syscall" -) - -// Conn represents a connection to the wayland display server. -type Conn struct { - conn *net.UnixConn - - done chan struct{} - doneOnce sync.Once - - mu sync.Mutex -} - -// Attach connects Conn to a wayland socket. -func (c *Conn) Attach(p string) (err error) { - c.mu.Lock() - defer c.mu.Unlock() - - if c.conn != nil { - return errors.New("socket already attached") - } - - c.conn, err = net.DialUnix("unix", nil, &net.UnixAddr{Name: p, Net: "unix"}) - return -} - -// Close releases resources and closes the connection to the wayland compositor. -func (c *Conn) Close() error { - c.mu.Lock() - defer c.mu.Unlock() - - if c.done == nil { - return errors.New("no socket bound") - } - - c.doneOnce.Do(func() { - c.done <- struct{}{} - <-c.done - }) - - // closed by wayland - runtime.SetFinalizer(c.conn, nil) - return nil -} - -// Bind binds the new socket to pathname. -func (c *Conn) Bind(pathname, appID, instanceID string) (*os.File, error) { - c.mu.Lock() - defer c.mu.Unlock() - - if c.conn == nil { - return nil, errors.New("socket not attached") - } - if c.done != nil { - return nil, errors.New("socket already bound") - } - - if rc, err := c.conn.SyscallConn(); err != nil { - // unreachable - return nil, err - } else { - c.done = make(chan struct{}) - return bindRawConn(c.done, rc, pathname, appID, instanceID) - } -} - -func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID string) (*os.File, error) { - var syncPipe [2]*os.File - - if r, w, err := os.Pipe(); err != nil { - return nil, err - } else { - syncPipe[0] = r - syncPipe[1] = w - } - - setupDone := make(chan error, 1) // does not block with c.done - - go func() { - if err := rc.Control(func(fd uintptr) { - // prevent runtime from closing the read end of sync fd - runtime.SetFinalizer(syncPipe[0], nil) - - // allow the Bind method to return after setup - setupDone <- bind(fd, p, appID, instanceID, syncPipe[0].Fd()) - close(setupDone) - - // keep socket alive until done is requested - <-done - runtime.KeepAlive(syncPipe[1]) - }); err != nil { - setupDone <- err - } - - // notify Close that rc.Control has returned - close(done) - }() - - // return write end of the pipe - return syncPipe[1], <-setupDone -} - -func bind(fd uintptr, p, appID, instanceID string, syncFd uintptr) error { - // ensure p is available - if f, err := os.Create(p); err != nil { - return err - } else if err = f.Close(); err != nil { - return err - } else if err = os.Remove(p); err != nil { - return err - } - - return bindWaylandFd(p, fd, appID, instanceID, syncFd) -} diff --git a/internal/system/wayland/security-context-v1-protocol.c b/internal/system/wayland/security-context-v1-protocol.c deleted file mode 100644 index 5b15b1b4..00000000 --- a/internal/system/wayland/security-context-v1-protocol.c +++ /dev/null @@ -1,74 +0,0 @@ -/* Generated by wayland-scanner 1.23.1 */ - -/* - * Copyright © 2021 Simon Ser - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice (including the next - * paragraph) shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ - -#include -#include -#include -#include "wayland-util.h" - -#ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ -#endif - -#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) -#define WL_PRIVATE __attribute__ ((visibility("hidden"))) -#else -#define WL_PRIVATE -#endif - -extern const struct wl_interface wp_security_context_v1_interface; - -static const struct wl_interface *security_context_v1_types[] = { - NULL, - &wp_security_context_v1_interface, - NULL, - NULL, -}; - -static const struct wl_message wp_security_context_manager_v1_requests[] = { - { "destroy", "", security_context_v1_types + 0 }, - { "create_listener", "nhh", security_context_v1_types + 1 }, -}; - -WL_PRIVATE const struct wl_interface wp_security_context_manager_v1_interface = { - "wp_security_context_manager_v1", 1, - 2, wp_security_context_manager_v1_requests, - 0, NULL, -}; - -static const struct wl_message wp_security_context_v1_requests[] = { - { "destroy", "", security_context_v1_types + 0 }, - { "set_sandbox_engine", "s", security_context_v1_types + 0 }, - { "set_app_id", "s", security_context_v1_types + 0 }, - { "set_instance_id", "s", security_context_v1_types + 0 }, - { "commit", "", security_context_v1_types + 0 }, -}; - -WL_PRIVATE const struct wl_interface wp_security_context_v1_interface = { - "wp_security_context_v1", 1, - 5, wp_security_context_v1_requests, - 0, NULL, -}; - diff --git a/internal/system/wayland/security-context-v1-protocol.h b/internal/system/wayland/security-context-v1-protocol.h deleted file mode 100644 index f973619e..00000000 --- a/internal/system/wayland/security-context-v1-protocol.h +++ /dev/null @@ -1,392 +0,0 @@ -/* Generated by wayland-scanner 1.23.1 */ - -#ifndef SECURITY_CONTEXT_V1_CLIENT_PROTOCOL_H -#define SECURITY_CONTEXT_V1_CLIENT_PROTOCOL_H - -#include -#include -#include "wayland-client.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @page page_security_context_v1 The security_context_v1 protocol - * @section page_ifaces_security_context_v1 Interfaces - * - @subpage page_iface_wp_security_context_manager_v1 - client security context manager - * - @subpage page_iface_wp_security_context_v1 - client security context - * @section page_copyright_security_context_v1 Copyright - *
- *
- * Copyright © 2021 Simon Ser
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice (including the next
- * paragraph) shall be included in all copies or substantial portions of the
- * Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- * 
- */ -struct wp_security_context_manager_v1; -struct wp_security_context_v1; - -#ifndef WP_SECURITY_CONTEXT_MANAGER_V1_INTERFACE -#define WP_SECURITY_CONTEXT_MANAGER_V1_INTERFACE -/** - * @page page_iface_wp_security_context_manager_v1 wp_security_context_manager_v1 - * @section page_iface_wp_security_context_manager_v1_desc Description - * - * This interface allows a client to register a new Wayland connection to - * the compositor and attach a security context to it. - * - * This is intended to be used by sandboxes. Sandbox engines attach a - * security context to all connections coming from inside the sandbox. The - * compositor can then restrict the features that the sandboxed connections - * can use. - * - * Compositors should forbid nesting multiple security contexts by not - * exposing wp_security_context_manager_v1 global to clients with a security - * context attached, or by sending the nested protocol error. Nested - * security contexts are dangerous because they can potentially allow - * privilege escalation of a sandboxed client. - * - * Warning! The protocol described in this file is currently in the testing - * phase. Backward compatible changes may be added together with the - * corresponding interface version bump. Backward incompatible changes can - * only be done by creating a new major version of the extension. - * @section page_iface_wp_security_context_manager_v1_api API - * See @ref iface_wp_security_context_manager_v1. - */ -/** - * @defgroup iface_wp_security_context_manager_v1 The wp_security_context_manager_v1 interface - * - * This interface allows a client to register a new Wayland connection to - * the compositor and attach a security context to it. - * - * This is intended to be used by sandboxes. Sandbox engines attach a - * security context to all connections coming from inside the sandbox. The - * compositor can then restrict the features that the sandboxed connections - * can use. - * - * Compositors should forbid nesting multiple security contexts by not - * exposing wp_security_context_manager_v1 global to clients with a security - * context attached, or by sending the nested protocol error. Nested - * security contexts are dangerous because they can potentially allow - * privilege escalation of a sandboxed client. - * - * Warning! The protocol described in this file is currently in the testing - * phase. Backward compatible changes may be added together with the - * corresponding interface version bump. Backward incompatible changes can - * only be done by creating a new major version of the extension. - */ -extern const struct wl_interface wp_security_context_manager_v1_interface; -#endif -#ifndef WP_SECURITY_CONTEXT_V1_INTERFACE -#define WP_SECURITY_CONTEXT_V1_INTERFACE -/** - * @page page_iface_wp_security_context_v1 wp_security_context_v1 - * @section page_iface_wp_security_context_v1_desc Description - * - * The security context allows a client to register a new client and attach - * security context metadata to the connections. - * - * When both are set, the combination of the application ID and the sandbox - * engine must uniquely identify an application. The same application ID - * will be used across instances (e.g. if the application is restarted, or - * if the application is started multiple times). - * - * When both are set, the combination of the instance ID and the sandbox - * engine must uniquely identify a running instance of an application. - * @section page_iface_wp_security_context_v1_api API - * See @ref iface_wp_security_context_v1. - */ -/** - * @defgroup iface_wp_security_context_v1 The wp_security_context_v1 interface - * - * The security context allows a client to register a new client and attach - * security context metadata to the connections. - * - * When both are set, the combination of the application ID and the sandbox - * engine must uniquely identify an application. The same application ID - * will be used across instances (e.g. if the application is restarted, or - * if the application is started multiple times). - * - * When both are set, the combination of the instance ID and the sandbox - * engine must uniquely identify a running instance of an application. - */ -extern const struct wl_interface wp_security_context_v1_interface; -#endif - -#ifndef WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_ENUM -#define WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_ENUM -enum wp_security_context_manager_v1_error { - /** - * listening socket FD is invalid - */ - WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_INVALID_LISTEN_FD = 1, - /** - * nested security contexts are forbidden - */ - WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_NESTED = 2, -}; -#endif /* WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_ENUM */ - -#define WP_SECURITY_CONTEXT_MANAGER_V1_DESTROY 0 -#define WP_SECURITY_CONTEXT_MANAGER_V1_CREATE_LISTENER 1 - - -/** - * @ingroup iface_wp_security_context_manager_v1 - */ -#define WP_SECURITY_CONTEXT_MANAGER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_security_context_manager_v1 - */ -#define WP_SECURITY_CONTEXT_MANAGER_V1_CREATE_LISTENER_SINCE_VERSION 1 - -/** @ingroup iface_wp_security_context_manager_v1 */ -static inline void -wp_security_context_manager_v1_set_user_data(struct wp_security_context_manager_v1 *wp_security_context_manager_v1, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) wp_security_context_manager_v1, user_data); -} - -/** @ingroup iface_wp_security_context_manager_v1 */ -static inline void * -wp_security_context_manager_v1_get_user_data(struct wp_security_context_manager_v1 *wp_security_context_manager_v1) -{ - return wl_proxy_get_user_data((struct wl_proxy *) wp_security_context_manager_v1); -} - -static inline uint32_t -wp_security_context_manager_v1_get_version(struct wp_security_context_manager_v1 *wp_security_context_manager_v1) -{ - return wl_proxy_get_version((struct wl_proxy *) wp_security_context_manager_v1); -} - -/** - * @ingroup iface_wp_security_context_manager_v1 - * - * Destroy the manager. This doesn't destroy objects created with the - * manager. - */ -static inline void -wp_security_context_manager_v1_destroy(struct wp_security_context_manager_v1 *wp_security_context_manager_v1) -{ - wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_manager_v1, - WP_SECURITY_CONTEXT_MANAGER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_manager_v1), WL_MARSHAL_FLAG_DESTROY); -} - -/** - * @ingroup iface_wp_security_context_manager_v1 - * - * Creates a new security context with a socket listening FD. - * - * The compositor will accept new client connections on listen_fd. - * listen_fd must be ready to accept new connections when this request is - * sent by the client. In other words, the client must call bind(2) and - * listen(2) before sending the FD. - * - * close_fd is a FD that will signal hangup when the compositor should stop - * accepting new connections on listen_fd. - * - * The compositor must continue to accept connections on listen_fd when - * the Wayland client which created the security context disconnects. - * - * After sending this request, closing listen_fd and close_fd remains the - * only valid operation on them. - */ -static inline struct wp_security_context_v1 * -wp_security_context_manager_v1_create_listener(struct wp_security_context_manager_v1 *wp_security_context_manager_v1, int32_t listen_fd, int32_t close_fd) -{ - struct wl_proxy *id; - - id = wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_manager_v1, - WP_SECURITY_CONTEXT_MANAGER_V1_CREATE_LISTENER, &wp_security_context_v1_interface, wl_proxy_get_version((struct wl_proxy *) wp_security_context_manager_v1), 0, NULL, listen_fd, close_fd); - - return (struct wp_security_context_v1 *) id; -} - -#ifndef WP_SECURITY_CONTEXT_V1_ERROR_ENUM -#define WP_SECURITY_CONTEXT_V1_ERROR_ENUM -enum wp_security_context_v1_error { - /** - * security context has already been committed - */ - WP_SECURITY_CONTEXT_V1_ERROR_ALREADY_USED = 1, - /** - * metadata has already been set - */ - WP_SECURITY_CONTEXT_V1_ERROR_ALREADY_SET = 2, - /** - * metadata is invalid - */ - WP_SECURITY_CONTEXT_V1_ERROR_INVALID_METADATA = 3, -}; -#endif /* WP_SECURITY_CONTEXT_V1_ERROR_ENUM */ - -#define WP_SECURITY_CONTEXT_V1_DESTROY 0 -#define WP_SECURITY_CONTEXT_V1_SET_SANDBOX_ENGINE 1 -#define WP_SECURITY_CONTEXT_V1_SET_APP_ID 2 -#define WP_SECURITY_CONTEXT_V1_SET_INSTANCE_ID 3 -#define WP_SECURITY_CONTEXT_V1_COMMIT 4 - - -/** - * @ingroup iface_wp_security_context_v1 - */ -#define WP_SECURITY_CONTEXT_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_security_context_v1 - */ -#define WP_SECURITY_CONTEXT_V1_SET_SANDBOX_ENGINE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_security_context_v1 - */ -#define WP_SECURITY_CONTEXT_V1_SET_APP_ID_SINCE_VERSION 1 -/** - * @ingroup iface_wp_security_context_v1 - */ -#define WP_SECURITY_CONTEXT_V1_SET_INSTANCE_ID_SINCE_VERSION 1 -/** - * @ingroup iface_wp_security_context_v1 - */ -#define WP_SECURITY_CONTEXT_V1_COMMIT_SINCE_VERSION 1 - -/** @ingroup iface_wp_security_context_v1 */ -static inline void -wp_security_context_v1_set_user_data(struct wp_security_context_v1 *wp_security_context_v1, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) wp_security_context_v1, user_data); -} - -/** @ingroup iface_wp_security_context_v1 */ -static inline void * -wp_security_context_v1_get_user_data(struct wp_security_context_v1 *wp_security_context_v1) -{ - return wl_proxy_get_user_data((struct wl_proxy *) wp_security_context_v1); -} - -static inline uint32_t -wp_security_context_v1_get_version(struct wp_security_context_v1 *wp_security_context_v1) -{ - return wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1); -} - -/** - * @ingroup iface_wp_security_context_v1 - * - * Destroy the security context object. - */ -static inline void -wp_security_context_v1_destroy(struct wp_security_context_v1 *wp_security_context_v1) -{ - wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, - WP_SECURITY_CONTEXT_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), WL_MARSHAL_FLAG_DESTROY); -} - -/** - * @ingroup iface_wp_security_context_v1 - * - * Attach a unique sandbox engine name to the security context. The name - * should follow the reverse-DNS style (e.g. "org.flatpak"). - * - * A list of well-known engines is maintained at: - * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md - * - * It is a protocol error to call this request twice. The already_set - * error is sent in this case. - */ -static inline void -wp_security_context_v1_set_sandbox_engine(struct wp_security_context_v1 *wp_security_context_v1, const char *name) -{ - wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, - WP_SECURITY_CONTEXT_V1_SET_SANDBOX_ENGINE, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), 0, name); -} - -/** - * @ingroup iface_wp_security_context_v1 - * - * Attach an application ID to the security context. - * - * The application ID is an opaque, sandbox-specific identifier for an - * application. See the well-known engines document for more details: - * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md - * - * The compositor may use the application ID to group clients belonging to - * the same security context application. - * - * Whether this request is optional or not depends on the sandbox engine used. - * - * It is a protocol error to call this request twice. The already_set - * error is sent in this case. - */ -static inline void -wp_security_context_v1_set_app_id(struct wp_security_context_v1 *wp_security_context_v1, const char *app_id) -{ - wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, - WP_SECURITY_CONTEXT_V1_SET_APP_ID, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), 0, app_id); -} - -/** - * @ingroup iface_wp_security_context_v1 - * - * Attach an instance ID to the security context. - * - * The instance ID is an opaque, sandbox-specific identifier for a running - * instance of an application. See the well-known engines document for - * more details: - * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md - * - * Whether this request is optional or not depends on the sandbox engine used. - * - * It is a protocol error to call this request twice. The already_set - * error is sent in this case. - */ -static inline void -wp_security_context_v1_set_instance_id(struct wp_security_context_v1 *wp_security_context_v1, const char *instance_id) -{ - wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, - WP_SECURITY_CONTEXT_V1_SET_INSTANCE_ID, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), 0, instance_id); -} - -/** - * @ingroup iface_wp_security_context_v1 - * - * Atomically register the new client and attach the security context - * metadata. - * - * If the provided metadata is inconsistent or does not match with out of - * band metadata (see - * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md), - * the invalid_metadata error may be sent eventually. - * - * It's a protocol error to send any request other than "destroy" after - * this request. In this case, the already_used error is sent. - */ -static inline void -wp_security_context_v1_commit(struct wp_security_context_v1 *wp_security_context_v1) -{ - wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, - WP_SECURITY_CONTEXT_V1_COMMIT, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), 0); -} - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/internal/system/wayland/wayland-client-helper.c b/internal/system/wayland/wayland-client-helper.c deleted file mode 100644 index 5a2c9a01..00000000 --- a/internal/system/wayland/wayland-client-helper.c +++ /dev/null @@ -1,96 +0,0 @@ -#include "wayland-client-helper.h" -#include -#include -#include -#include -#include -#include - -#include "security-context-v1-protocol.h" -#include - -static void registry_handle_global(void *data, struct wl_registry *registry, - uint32_t name, const char *interface, - uint32_t version) { - struct wp_security_context_manager_v1 **out = data; - - if (strcmp(interface, wp_security_context_manager_v1_interface.name) == 0) - *out = wl_registry_bind(registry, name, - &wp_security_context_manager_v1_interface, 1); -} - -static void registry_handle_global_remove(void *data, - struct wl_registry *registry, - uint32_t name) {} /* no-op */ - -static const struct wl_registry_listener registry_listener = { - .global = registry_handle_global, - .global_remove = registry_handle_global_remove, -}; - -int32_t hakurei_bind_wayland_fd(char *socket_path, int fd, const char *app_id, - const char *instance_id, int sync_fd) { - int32_t res = 0; /* refer to resErr for corresponding Go error */ - - struct wl_display *display; - display = wl_display_connect_to_fd(fd); - if (!display) { - res = 1; - goto out; - }; - - struct wl_registry *registry; - registry = wl_display_get_registry(display); - - struct wp_security_context_manager_v1 *security_context_manager = NULL; - wl_registry_add_listener(registry, ®istry_listener, - &security_context_manager); - int ret; - ret = wl_display_roundtrip(display); - wl_registry_destroy(registry); - if (ret < 0) - goto out; - - if (!security_context_manager) { - res = 2; - goto out; - } - - int listen_fd = -1; - listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (listen_fd < 0) - goto out; - - struct sockaddr_un sockaddr = {0}; - 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) - goto out; - - if (listen(listen_fd, 0) != 0) - goto out; - - struct wp_security_context_v1 *security_context; - security_context = wp_security_context_manager_v1_create_listener( - security_context_manager, listen_fd, sync_fd); - wp_security_context_v1_set_sandbox_engine(security_context, "app.hakurei"); - wp_security_context_v1_set_app_id(security_context, app_id); - wp_security_context_v1_set_instance_id(security_context, instance_id); - wp_security_context_v1_commit(security_context); - wp_security_context_v1_destroy(security_context); - if (wl_display_roundtrip(display) < 0) - goto out; - -out: - if (listen_fd >= 0) - close(listen_fd); - if (security_context_manager) - wp_security_context_manager_v1_destroy(security_context_manager); - if (display) - wl_display_disconnect(display); - - free((void *)socket_path); - free((void *)app_id); - free((void *)instance_id); - return res; -} diff --git a/internal/system/wayland/wayland-client-helper.h b/internal/system/wayland/wayland-client-helper.h deleted file mode 100644 index eae8936d..00000000 --- a/internal/system/wayland/wayland-client-helper.h +++ /dev/null @@ -1,4 +0,0 @@ -#include - -int32_t hakurei_bind_wayland_fd(char *socket_path, int fd, const char *app_id, - const char *instance_id, int sync_fd); \ No newline at end of file diff --git a/internal/system/wayland/wayland.go b/internal/system/wayland/wayland.go deleted file mode 100644 index 7a8694cd..00000000 --- a/internal/system/wayland/wayland.go +++ /dev/null @@ -1,48 +0,0 @@ -// Package wayland implements Wayland security_context_v1 protocol. -package wayland - -//go:generate sh -c "wayland-scanner client-header `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.h" -//go:generate sh -c "wayland-scanner private-code `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.c" - -/* -#cgo linux pkg-config: --static wayland-client -#cgo freebsd openbsd LDFLAGS: -lwayland-client - -#include "wayland-client-helper.h" -*/ -import "C" -import ( - "errors" - "strings" - "syscall" -) - -const ( - // Display contains the name of the server socket - // (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1147) - // which is concatenated with XDG_RUNTIME_DIR - // (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1171) - // or used as-is if absolute - // (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1176). - Display = "WAYLAND_DISPLAY" - - // FallbackName is used as the wayland socket name if WAYLAND_DISPLAY is unset - // (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1149). - FallbackName = "wayland-0" -) - -var resErr = [...]error{ - 0: nil, - 1: errors.New("wl_display_connect_to_fd() failed"), - 2: errors.New("wp_security_context_v1 not available"), -} - -func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFd uintptr) error { - if hasNull(appID) || hasNull(instanceID) { - return syscall.EINVAL - } - res := C.hakurei_bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFd)) - return resErr[int32(res)] -} - -func hasNull(s string) bool { return strings.IndexByte(s, 0) > -1 } diff --git a/internal/system/wayland_test.go b/internal/system/wayland_test.go index 083c8ba7..4e7db513 100644 --- a/internal/system/wayland_test.go +++ b/internal/system/wayland_test.go @@ -6,8 +6,8 @@ import ( "testing" "hakurei.app/container/stub" - "hakurei.app/internal/system/acl" - "hakurei.app/internal/system/wayland" + "hakurei.app/internal/acl" + "hakurei.app/internal/wayland" ) type stubWaylandConn struct { diff --git a/internal/system/xcb/changehosts.go b/internal/system/xcb/changehosts.go deleted file mode 100644 index 8b05614b..00000000 --- a/internal/system/xcb/changehosts.go +++ /dev/null @@ -1,17 +0,0 @@ -package xcb - -import "errors" - -var ErrChangeHosts = errors.New("xcb_change_hosts() failed") - -func ChangeHosts(mode HostMode, family Family, address string) error { - conn := new(connection) - if err := conn.connect(); err != nil { - conn.disconnect() - return err - } else { - defer conn.disconnect() - } - - return conn.changeHostsChecked(mode, family, address) -} diff --git a/internal/system/xcb/xcb.go b/internal/system/xcb/xcb.go deleted file mode 100644 index 3dc62c3f..00000000 --- a/internal/system/xcb/xcb.go +++ /dev/null @@ -1,127 +0,0 @@ -// Package xcb implements X11 ChangeHosts via libxcb. -package xcb - -import ( - "runtime" - "unsafe" -) - -/* -#cgo linux pkg-config: --static xcb - -#include -#include - -static int hakurei_xcb_change_hosts_checked(xcb_connection_t *c, - uint8_t mode, uint8_t family, - uint16_t address_len, const uint8_t *address) { - int ret; - xcb_generic_error_t *e; - xcb_void_cookie_t cookie; - - cookie = xcb_change_hosts_checked(c, mode, family, address_len, address); - free((void *)address); - - ret = xcb_connection_has_error(c); - if (ret != 0) - return ret; - - e = xcb_request_check(c, cookie); - if (e != NULL) { - // don't want to deal with xcb errors - free((void *)e); - ret = -1; - } - - return ret; -} -*/ -import "C" - -const ( - HostModeInsert = C.XCB_HOST_MODE_INSERT - HostModeDelete = C.XCB_HOST_MODE_DELETE - - FamilyInternet = C.XCB_FAMILY_INTERNET - FamilyDecnet = C.XCB_FAMILY_DECNET - FamilyChaos = C.XCB_FAMILY_CHAOS - FamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED - FamilyInternet6 = C.XCB_FAMILY_INTERNET_6 -) - -type ( - HostMode = C.xcb_host_mode_t - Family = C.xcb_family_t -) - -func (conn *connection) changeHostsChecked(mode HostMode, family Family, address string) error { - ret := C.hakurei_xcb_change_hosts_checked( - conn.c, - C.uint8_t(mode), - C.uint8_t(family), - C.uint16_t(len(address)), - (*C.uint8_t)(unsafe.Pointer(C.CString(address))), - ) - switch ret { - case 0: - return nil - case -1: - return ErrChangeHosts - default: - return ConnectionError(ret) - } -} - -type connection struct{ c *C.xcb_connection_t } - -func (conn *connection) connect() error { - conn.c = C.xcb_connect(nil, nil) - runtime.SetFinalizer(conn, (*connection).disconnect) - return conn.hasError() -} - -func (conn *connection) hasError() error { - ret := C.xcb_connection_has_error(conn.c) - if ret == 0 { - return nil - } - return ConnectionError(ret) -} - -func (conn *connection) disconnect() { - C.xcb_disconnect(conn.c) - - // no need for a finalizer anymore - runtime.SetFinalizer(conn, nil) -} - -const ( - ConnError ConnectionError = C.XCB_CONN_ERROR - ConnClosedExtNotSupported ConnectionError = C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED - ConnClosedMemInsufficient ConnectionError = C.XCB_CONN_CLOSED_MEM_INSUFFICIENT - ConnClosedReqLenExceed ConnectionError = C.XCB_CONN_CLOSED_REQ_LEN_EXCEED - ConnClosedParseErr ConnectionError = C.XCB_CONN_CLOSED_PARSE_ERR - ConnClosedInvalidScreen ConnectionError = C.XCB_CONN_CLOSED_INVALID_SCREEN -) - -// ConnectionError represents an error returned by xcb_connection_has_error. -type ConnectionError int - -func (ce ConnectionError) Error() string { - switch ce { - case ConnError: - return "connection error" - case ConnClosedExtNotSupported: - return "extension not supported" - case ConnClosedMemInsufficient: - return "memory not available" - case ConnClosedReqLenExceed: - return "request length exceeded" - case ConnClosedParseErr: - return "invalid display string" - case ConnClosedInvalidScreen: - return "server has no screen matching display" - default: - return "generic X11 failure" - } -} diff --git a/internal/system/xhost.go b/internal/system/xhost.go index 27e60c2b..be44ce1a 100644 --- a/internal/system/xhost.go +++ b/internal/system/xhost.go @@ -2,7 +2,7 @@ package system import ( "hakurei.app/hst" - "hakurei.app/internal/system/xcb" + "hakurei.app/internal/xcb" ) // ChangeHosts inserts the target user into X11 hosts and deletes it once its [Enablement] is no longer satisfied. diff --git a/internal/system/xhost_test.go b/internal/system/xhost_test.go index 853da651..75ad5b9b 100644 --- a/internal/system/xhost_test.go +++ b/internal/system/xhost_test.go @@ -5,7 +5,7 @@ import ( "hakurei.app/container/stub" "hakurei.app/hst" - "hakurei.app/internal/system/xcb" + "hakurei.app/internal/xcb" ) func TestXHostOp(t *testing.T) { diff --git a/internal/wayland/conn.go b/internal/wayland/conn.go new file mode 100644 index 00000000..c55fe704 --- /dev/null +++ b/internal/wayland/conn.go @@ -0,0 +1,122 @@ +package wayland + +import ( + "errors" + "net" + "os" + "runtime" + "sync" + "syscall" +) + +// Conn represents a connection to the wayland display server. +type Conn struct { + conn *net.UnixConn + + done chan struct{} + doneOnce sync.Once + + mu sync.Mutex +} + +// Attach connects Conn to a wayland socket. +func (c *Conn) Attach(p string) (err error) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.conn != nil { + return errors.New("socket already attached") + } + + c.conn, err = net.DialUnix("unix", nil, &net.UnixAddr{Name: p, Net: "unix"}) + return +} + +// Close releases resources and closes the connection to the wayland compositor. +func (c *Conn) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.done == nil { + return errors.New("no socket bound") + } + + c.doneOnce.Do(func() { + c.done <- struct{}{} + <-c.done + }) + + // closed by wayland + runtime.SetFinalizer(c.conn, nil) + return nil +} + +// Bind binds the new socket to pathname. +func (c *Conn) Bind(pathname, appID, instanceID string) (*os.File, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.conn == nil { + return nil, errors.New("socket not attached") + } + if c.done != nil { + return nil, errors.New("socket already bound") + } + + if rc, err := c.conn.SyscallConn(); err != nil { + // unreachable + return nil, err + } else { + c.done = make(chan struct{}) + return bindRawConn(c.done, rc, pathname, appID, instanceID) + } +} + +func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID string) (*os.File, error) { + var syncPipe [2]*os.File + + if r, w, err := os.Pipe(); err != nil { + return nil, err + } else { + syncPipe[0] = r + syncPipe[1] = w + } + + setupDone := make(chan error, 1) // does not block with c.done + + go func() { + if err := rc.Control(func(fd uintptr) { + // prevent runtime from closing the read end of sync fd + runtime.SetFinalizer(syncPipe[0], nil) + + // allow the Bind method to return after setup + setupDone <- bind(fd, p, appID, instanceID, syncPipe[0].Fd()) + close(setupDone) + + // keep socket alive until done is requested + <-done + runtime.KeepAlive(syncPipe[1]) + }); err != nil { + setupDone <- err + } + + // notify Close that rc.Control has returned + close(done) + }() + + // return write end of the pipe + return syncPipe[1], <-setupDone +} + +func bind(fd uintptr, p, appID, instanceID string, syncFd uintptr) error { + // ensure p is available + if f, err := os.Create(p); err != nil { + return err + } else if err = f.Close(); err != nil { + return err + } else if err = os.Remove(p); err != nil { + return err + } + + return bindWaylandFd(p, fd, appID, instanceID, syncFd) +} diff --git a/internal/wayland/security-context-v1-protocol.c b/internal/wayland/security-context-v1-protocol.c new file mode 100644 index 00000000..5b15b1b4 --- /dev/null +++ b/internal/wayland/security-context-v1-protocol.c @@ -0,0 +1,74 @@ +/* Generated by wayland-scanner 1.23.1 */ + +/* + * Copyright © 2021 Simon Ser + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wp_security_context_v1_interface; + +static const struct wl_interface *security_context_v1_types[] = { + NULL, + &wp_security_context_v1_interface, + NULL, + NULL, +}; + +static const struct wl_message wp_security_context_manager_v1_requests[] = { + { "destroy", "", security_context_v1_types + 0 }, + { "create_listener", "nhh", security_context_v1_types + 1 }, +}; + +WL_PRIVATE const struct wl_interface wp_security_context_manager_v1_interface = { + "wp_security_context_manager_v1", 1, + 2, wp_security_context_manager_v1_requests, + 0, NULL, +}; + +static const struct wl_message wp_security_context_v1_requests[] = { + { "destroy", "", security_context_v1_types + 0 }, + { "set_sandbox_engine", "s", security_context_v1_types + 0 }, + { "set_app_id", "s", security_context_v1_types + 0 }, + { "set_instance_id", "s", security_context_v1_types + 0 }, + { "commit", "", security_context_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface wp_security_context_v1_interface = { + "wp_security_context_v1", 1, + 5, wp_security_context_v1_requests, + 0, NULL, +}; + diff --git a/internal/wayland/security-context-v1-protocol.h b/internal/wayland/security-context-v1-protocol.h new file mode 100644 index 00000000..f973619e --- /dev/null +++ b/internal/wayland/security-context-v1-protocol.h @@ -0,0 +1,392 @@ +/* Generated by wayland-scanner 1.23.1 */ + +#ifndef SECURITY_CONTEXT_V1_CLIENT_PROTOCOL_H +#define SECURITY_CONTEXT_V1_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_security_context_v1 The security_context_v1 protocol + * @section page_ifaces_security_context_v1 Interfaces + * - @subpage page_iface_wp_security_context_manager_v1 - client security context manager + * - @subpage page_iface_wp_security_context_v1 - client security context + * @section page_copyright_security_context_v1 Copyright + *
+ *
+ * Copyright © 2021 Simon Ser
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * 
+ */ +struct wp_security_context_manager_v1; +struct wp_security_context_v1; + +#ifndef WP_SECURITY_CONTEXT_MANAGER_V1_INTERFACE +#define WP_SECURITY_CONTEXT_MANAGER_V1_INTERFACE +/** + * @page page_iface_wp_security_context_manager_v1 wp_security_context_manager_v1 + * @section page_iface_wp_security_context_manager_v1_desc Description + * + * This interface allows a client to register a new Wayland connection to + * the compositor and attach a security context to it. + * + * This is intended to be used by sandboxes. Sandbox engines attach a + * security context to all connections coming from inside the sandbox. The + * compositor can then restrict the features that the sandboxed connections + * can use. + * + * Compositors should forbid nesting multiple security contexts by not + * exposing wp_security_context_manager_v1 global to clients with a security + * context attached, or by sending the nested protocol error. Nested + * security contexts are dangerous because they can potentially allow + * privilege escalation of a sandboxed client. + * + * Warning! The protocol described in this file is currently in the testing + * phase. Backward compatible changes may be added together with the + * corresponding interface version bump. Backward incompatible changes can + * only be done by creating a new major version of the extension. + * @section page_iface_wp_security_context_manager_v1_api API + * See @ref iface_wp_security_context_manager_v1. + */ +/** + * @defgroup iface_wp_security_context_manager_v1 The wp_security_context_manager_v1 interface + * + * This interface allows a client to register a new Wayland connection to + * the compositor and attach a security context to it. + * + * This is intended to be used by sandboxes. Sandbox engines attach a + * security context to all connections coming from inside the sandbox. The + * compositor can then restrict the features that the sandboxed connections + * can use. + * + * Compositors should forbid nesting multiple security contexts by not + * exposing wp_security_context_manager_v1 global to clients with a security + * context attached, or by sending the nested protocol error. Nested + * security contexts are dangerous because they can potentially allow + * privilege escalation of a sandboxed client. + * + * Warning! The protocol described in this file is currently in the testing + * phase. Backward compatible changes may be added together with the + * corresponding interface version bump. Backward incompatible changes can + * only be done by creating a new major version of the extension. + */ +extern const struct wl_interface wp_security_context_manager_v1_interface; +#endif +#ifndef WP_SECURITY_CONTEXT_V1_INTERFACE +#define WP_SECURITY_CONTEXT_V1_INTERFACE +/** + * @page page_iface_wp_security_context_v1 wp_security_context_v1 + * @section page_iface_wp_security_context_v1_desc Description + * + * The security context allows a client to register a new client and attach + * security context metadata to the connections. + * + * When both are set, the combination of the application ID and the sandbox + * engine must uniquely identify an application. The same application ID + * will be used across instances (e.g. if the application is restarted, or + * if the application is started multiple times). + * + * When both are set, the combination of the instance ID and the sandbox + * engine must uniquely identify a running instance of an application. + * @section page_iface_wp_security_context_v1_api API + * See @ref iface_wp_security_context_v1. + */ +/** + * @defgroup iface_wp_security_context_v1 The wp_security_context_v1 interface + * + * The security context allows a client to register a new client and attach + * security context metadata to the connections. + * + * When both are set, the combination of the application ID and the sandbox + * engine must uniquely identify an application. The same application ID + * will be used across instances (e.g. if the application is restarted, or + * if the application is started multiple times). + * + * When both are set, the combination of the instance ID and the sandbox + * engine must uniquely identify a running instance of an application. + */ +extern const struct wl_interface wp_security_context_v1_interface; +#endif + +#ifndef WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_ENUM +#define WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_ENUM +enum wp_security_context_manager_v1_error { + /** + * listening socket FD is invalid + */ + WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_INVALID_LISTEN_FD = 1, + /** + * nested security contexts are forbidden + */ + WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_NESTED = 2, +}; +#endif /* WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_ENUM */ + +#define WP_SECURITY_CONTEXT_MANAGER_V1_DESTROY 0 +#define WP_SECURITY_CONTEXT_MANAGER_V1_CREATE_LISTENER 1 + + +/** + * @ingroup iface_wp_security_context_manager_v1 + */ +#define WP_SECURITY_CONTEXT_MANAGER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_wp_security_context_manager_v1 + */ +#define WP_SECURITY_CONTEXT_MANAGER_V1_CREATE_LISTENER_SINCE_VERSION 1 + +/** @ingroup iface_wp_security_context_manager_v1 */ +static inline void +wp_security_context_manager_v1_set_user_data(struct wp_security_context_manager_v1 *wp_security_context_manager_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) wp_security_context_manager_v1, user_data); +} + +/** @ingroup iface_wp_security_context_manager_v1 */ +static inline void * +wp_security_context_manager_v1_get_user_data(struct wp_security_context_manager_v1 *wp_security_context_manager_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) wp_security_context_manager_v1); +} + +static inline uint32_t +wp_security_context_manager_v1_get_version(struct wp_security_context_manager_v1 *wp_security_context_manager_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) wp_security_context_manager_v1); +} + +/** + * @ingroup iface_wp_security_context_manager_v1 + * + * Destroy the manager. This doesn't destroy objects created with the + * manager. + */ +static inline void +wp_security_context_manager_v1_destroy(struct wp_security_context_manager_v1 *wp_security_context_manager_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_manager_v1, + WP_SECURITY_CONTEXT_MANAGER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_manager_v1), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_wp_security_context_manager_v1 + * + * Creates a new security context with a socket listening FD. + * + * The compositor will accept new client connections on listen_fd. + * listen_fd must be ready to accept new connections when this request is + * sent by the client. In other words, the client must call bind(2) and + * listen(2) before sending the FD. + * + * close_fd is a FD that will signal hangup when the compositor should stop + * accepting new connections on listen_fd. + * + * The compositor must continue to accept connections on listen_fd when + * the Wayland client which created the security context disconnects. + * + * After sending this request, closing listen_fd and close_fd remains the + * only valid operation on them. + */ +static inline struct wp_security_context_v1 * +wp_security_context_manager_v1_create_listener(struct wp_security_context_manager_v1 *wp_security_context_manager_v1, int32_t listen_fd, int32_t close_fd) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_manager_v1, + WP_SECURITY_CONTEXT_MANAGER_V1_CREATE_LISTENER, &wp_security_context_v1_interface, wl_proxy_get_version((struct wl_proxy *) wp_security_context_manager_v1), 0, NULL, listen_fd, close_fd); + + return (struct wp_security_context_v1 *) id; +} + +#ifndef WP_SECURITY_CONTEXT_V1_ERROR_ENUM +#define WP_SECURITY_CONTEXT_V1_ERROR_ENUM +enum wp_security_context_v1_error { + /** + * security context has already been committed + */ + WP_SECURITY_CONTEXT_V1_ERROR_ALREADY_USED = 1, + /** + * metadata has already been set + */ + WP_SECURITY_CONTEXT_V1_ERROR_ALREADY_SET = 2, + /** + * metadata is invalid + */ + WP_SECURITY_CONTEXT_V1_ERROR_INVALID_METADATA = 3, +}; +#endif /* WP_SECURITY_CONTEXT_V1_ERROR_ENUM */ + +#define WP_SECURITY_CONTEXT_V1_DESTROY 0 +#define WP_SECURITY_CONTEXT_V1_SET_SANDBOX_ENGINE 1 +#define WP_SECURITY_CONTEXT_V1_SET_APP_ID 2 +#define WP_SECURITY_CONTEXT_V1_SET_INSTANCE_ID 3 +#define WP_SECURITY_CONTEXT_V1_COMMIT 4 + + +/** + * @ingroup iface_wp_security_context_v1 + */ +#define WP_SECURITY_CONTEXT_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_wp_security_context_v1 + */ +#define WP_SECURITY_CONTEXT_V1_SET_SANDBOX_ENGINE_SINCE_VERSION 1 +/** + * @ingroup iface_wp_security_context_v1 + */ +#define WP_SECURITY_CONTEXT_V1_SET_APP_ID_SINCE_VERSION 1 +/** + * @ingroup iface_wp_security_context_v1 + */ +#define WP_SECURITY_CONTEXT_V1_SET_INSTANCE_ID_SINCE_VERSION 1 +/** + * @ingroup iface_wp_security_context_v1 + */ +#define WP_SECURITY_CONTEXT_V1_COMMIT_SINCE_VERSION 1 + +/** @ingroup iface_wp_security_context_v1 */ +static inline void +wp_security_context_v1_set_user_data(struct wp_security_context_v1 *wp_security_context_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) wp_security_context_v1, user_data); +} + +/** @ingroup iface_wp_security_context_v1 */ +static inline void * +wp_security_context_v1_get_user_data(struct wp_security_context_v1 *wp_security_context_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) wp_security_context_v1); +} + +static inline uint32_t +wp_security_context_v1_get_version(struct wp_security_context_v1 *wp_security_context_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1); +} + +/** + * @ingroup iface_wp_security_context_v1 + * + * Destroy the security context object. + */ +static inline void +wp_security_context_v1_destroy(struct wp_security_context_v1 *wp_security_context_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, + WP_SECURITY_CONTEXT_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_wp_security_context_v1 + * + * Attach a unique sandbox engine name to the security context. The name + * should follow the reverse-DNS style (e.g. "org.flatpak"). + * + * A list of well-known engines is maintained at: + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md + * + * It is a protocol error to call this request twice. The already_set + * error is sent in this case. + */ +static inline void +wp_security_context_v1_set_sandbox_engine(struct wp_security_context_v1 *wp_security_context_v1, const char *name) +{ + wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, + WP_SECURITY_CONTEXT_V1_SET_SANDBOX_ENGINE, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), 0, name); +} + +/** + * @ingroup iface_wp_security_context_v1 + * + * Attach an application ID to the security context. + * + * The application ID is an opaque, sandbox-specific identifier for an + * application. See the well-known engines document for more details: + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md + * + * The compositor may use the application ID to group clients belonging to + * the same security context application. + * + * Whether this request is optional or not depends on the sandbox engine used. + * + * It is a protocol error to call this request twice. The already_set + * error is sent in this case. + */ +static inline void +wp_security_context_v1_set_app_id(struct wp_security_context_v1 *wp_security_context_v1, const char *app_id) +{ + wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, + WP_SECURITY_CONTEXT_V1_SET_APP_ID, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), 0, app_id); +} + +/** + * @ingroup iface_wp_security_context_v1 + * + * Attach an instance ID to the security context. + * + * The instance ID is an opaque, sandbox-specific identifier for a running + * instance of an application. See the well-known engines document for + * more details: + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md + * + * Whether this request is optional or not depends on the sandbox engine used. + * + * It is a protocol error to call this request twice. The already_set + * error is sent in this case. + */ +static inline void +wp_security_context_v1_set_instance_id(struct wp_security_context_v1 *wp_security_context_v1, const char *instance_id) +{ + wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, + WP_SECURITY_CONTEXT_V1_SET_INSTANCE_ID, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), 0, instance_id); +} + +/** + * @ingroup iface_wp_security_context_v1 + * + * Atomically register the new client and attach the security context + * metadata. + * + * If the provided metadata is inconsistent or does not match with out of + * band metadata (see + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md), + * the invalid_metadata error may be sent eventually. + * + * It's a protocol error to send any request other than "destroy" after + * this request. In this case, the already_used error is sent. + */ +static inline void +wp_security_context_v1_commit(struct wp_security_context_v1 *wp_security_context_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, + WP_SECURITY_CONTEXT_V1_COMMIT, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), 0); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/internal/wayland/wayland-client-helper.c b/internal/wayland/wayland-client-helper.c new file mode 100644 index 00000000..5a2c9a01 --- /dev/null +++ b/internal/wayland/wayland-client-helper.c @@ -0,0 +1,96 @@ +#include "wayland-client-helper.h" +#include +#include +#include +#include +#include +#include + +#include "security-context-v1-protocol.h" +#include + +static void registry_handle_global(void *data, struct wl_registry *registry, + uint32_t name, const char *interface, + uint32_t version) { + struct wp_security_context_manager_v1 **out = data; + + if (strcmp(interface, wp_security_context_manager_v1_interface.name) == 0) + *out = wl_registry_bind(registry, name, + &wp_security_context_manager_v1_interface, 1); +} + +static void registry_handle_global_remove(void *data, + struct wl_registry *registry, + uint32_t name) {} /* no-op */ + +static const struct wl_registry_listener registry_listener = { + .global = registry_handle_global, + .global_remove = registry_handle_global_remove, +}; + +int32_t hakurei_bind_wayland_fd(char *socket_path, int fd, const char *app_id, + const char *instance_id, int sync_fd) { + int32_t res = 0; /* refer to resErr for corresponding Go error */ + + struct wl_display *display; + display = wl_display_connect_to_fd(fd); + if (!display) { + res = 1; + goto out; + }; + + struct wl_registry *registry; + registry = wl_display_get_registry(display); + + struct wp_security_context_manager_v1 *security_context_manager = NULL; + wl_registry_add_listener(registry, ®istry_listener, + &security_context_manager); + int ret; + ret = wl_display_roundtrip(display); + wl_registry_destroy(registry); + if (ret < 0) + goto out; + + if (!security_context_manager) { + res = 2; + goto out; + } + + int listen_fd = -1; + listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (listen_fd < 0) + goto out; + + struct sockaddr_un sockaddr = {0}; + 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) + goto out; + + if (listen(listen_fd, 0) != 0) + goto out; + + struct wp_security_context_v1 *security_context; + security_context = wp_security_context_manager_v1_create_listener( + security_context_manager, listen_fd, sync_fd); + wp_security_context_v1_set_sandbox_engine(security_context, "app.hakurei"); + wp_security_context_v1_set_app_id(security_context, app_id); + wp_security_context_v1_set_instance_id(security_context, instance_id); + wp_security_context_v1_commit(security_context); + wp_security_context_v1_destroy(security_context); + if (wl_display_roundtrip(display) < 0) + goto out; + +out: + if (listen_fd >= 0) + close(listen_fd); + if (security_context_manager) + wp_security_context_manager_v1_destroy(security_context_manager); + if (display) + wl_display_disconnect(display); + + free((void *)socket_path); + free((void *)app_id); + free((void *)instance_id); + return res; +} diff --git a/internal/wayland/wayland-client-helper.h b/internal/wayland/wayland-client-helper.h new file mode 100644 index 00000000..eae8936d --- /dev/null +++ b/internal/wayland/wayland-client-helper.h @@ -0,0 +1,4 @@ +#include + +int32_t hakurei_bind_wayland_fd(char *socket_path, int fd, const char *app_id, + const char *instance_id, int sync_fd); \ No newline at end of file diff --git a/internal/wayland/wayland.go b/internal/wayland/wayland.go new file mode 100644 index 00000000..7a8694cd --- /dev/null +++ b/internal/wayland/wayland.go @@ -0,0 +1,48 @@ +// Package wayland implements Wayland security_context_v1 protocol. +package wayland + +//go:generate sh -c "wayland-scanner client-header `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.h" +//go:generate sh -c "wayland-scanner private-code `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.c" + +/* +#cgo linux pkg-config: --static wayland-client +#cgo freebsd openbsd LDFLAGS: -lwayland-client + +#include "wayland-client-helper.h" +*/ +import "C" +import ( + "errors" + "strings" + "syscall" +) + +const ( + // Display contains the name of the server socket + // (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1147) + // which is concatenated with XDG_RUNTIME_DIR + // (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1171) + // or used as-is if absolute + // (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1176). + Display = "WAYLAND_DISPLAY" + + // FallbackName is used as the wayland socket name if WAYLAND_DISPLAY is unset + // (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1149). + FallbackName = "wayland-0" +) + +var resErr = [...]error{ + 0: nil, + 1: errors.New("wl_display_connect_to_fd() failed"), + 2: errors.New("wp_security_context_v1 not available"), +} + +func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFd uintptr) error { + if hasNull(appID) || hasNull(instanceID) { + return syscall.EINVAL + } + res := C.hakurei_bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFd)) + return resErr[int32(res)] +} + +func hasNull(s string) bool { return strings.IndexByte(s, 0) > -1 } diff --git a/internal/xcb/changehosts.go b/internal/xcb/changehosts.go new file mode 100644 index 00000000..8b05614b --- /dev/null +++ b/internal/xcb/changehosts.go @@ -0,0 +1,17 @@ +package xcb + +import "errors" + +var ErrChangeHosts = errors.New("xcb_change_hosts() failed") + +func ChangeHosts(mode HostMode, family Family, address string) error { + conn := new(connection) + if err := conn.connect(); err != nil { + conn.disconnect() + return err + } else { + defer conn.disconnect() + } + + return conn.changeHostsChecked(mode, family, address) +} diff --git a/internal/xcb/xcb.go b/internal/xcb/xcb.go new file mode 100644 index 00000000..3dc62c3f --- /dev/null +++ b/internal/xcb/xcb.go @@ -0,0 +1,127 @@ +// Package xcb implements X11 ChangeHosts via libxcb. +package xcb + +import ( + "runtime" + "unsafe" +) + +/* +#cgo linux pkg-config: --static xcb + +#include +#include + +static int hakurei_xcb_change_hosts_checked(xcb_connection_t *c, + uint8_t mode, uint8_t family, + uint16_t address_len, const uint8_t *address) { + int ret; + xcb_generic_error_t *e; + xcb_void_cookie_t cookie; + + cookie = xcb_change_hosts_checked(c, mode, family, address_len, address); + free((void *)address); + + ret = xcb_connection_has_error(c); + if (ret != 0) + return ret; + + e = xcb_request_check(c, cookie); + if (e != NULL) { + // don't want to deal with xcb errors + free((void *)e); + ret = -1; + } + + return ret; +} +*/ +import "C" + +const ( + HostModeInsert = C.XCB_HOST_MODE_INSERT + HostModeDelete = C.XCB_HOST_MODE_DELETE + + FamilyInternet = C.XCB_FAMILY_INTERNET + FamilyDecnet = C.XCB_FAMILY_DECNET + FamilyChaos = C.XCB_FAMILY_CHAOS + FamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED + FamilyInternet6 = C.XCB_FAMILY_INTERNET_6 +) + +type ( + HostMode = C.xcb_host_mode_t + Family = C.xcb_family_t +) + +func (conn *connection) changeHostsChecked(mode HostMode, family Family, address string) error { + ret := C.hakurei_xcb_change_hosts_checked( + conn.c, + C.uint8_t(mode), + C.uint8_t(family), + C.uint16_t(len(address)), + (*C.uint8_t)(unsafe.Pointer(C.CString(address))), + ) + switch ret { + case 0: + return nil + case -1: + return ErrChangeHosts + default: + return ConnectionError(ret) + } +} + +type connection struct{ c *C.xcb_connection_t } + +func (conn *connection) connect() error { + conn.c = C.xcb_connect(nil, nil) + runtime.SetFinalizer(conn, (*connection).disconnect) + return conn.hasError() +} + +func (conn *connection) hasError() error { + ret := C.xcb_connection_has_error(conn.c) + if ret == 0 { + return nil + } + return ConnectionError(ret) +} + +func (conn *connection) disconnect() { + C.xcb_disconnect(conn.c) + + // no need for a finalizer anymore + runtime.SetFinalizer(conn, nil) +} + +const ( + ConnError ConnectionError = C.XCB_CONN_ERROR + ConnClosedExtNotSupported ConnectionError = C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED + ConnClosedMemInsufficient ConnectionError = C.XCB_CONN_CLOSED_MEM_INSUFFICIENT + ConnClosedReqLenExceed ConnectionError = C.XCB_CONN_CLOSED_REQ_LEN_EXCEED + ConnClosedParseErr ConnectionError = C.XCB_CONN_CLOSED_PARSE_ERR + ConnClosedInvalidScreen ConnectionError = C.XCB_CONN_CLOSED_INVALID_SCREEN +) + +// ConnectionError represents an error returned by xcb_connection_has_error. +type ConnectionError int + +func (ce ConnectionError) Error() string { + switch ce { + case ConnError: + return "connection error" + case ConnClosedExtNotSupported: + return "extension not supported" + case ConnClosedMemInsufficient: + return "memory not available" + case ConnClosedReqLenExceed: + return "request length exceeded" + case ConnClosedParseErr: + return "invalid display string" + case ConnClosedInvalidScreen: + return "server has no screen matching display" + default: + return "generic X11 failure" + } +} diff --git a/system/acl/deprecated.go b/system/acl/deprecated.go index 4119057b..1981eb72 100644 --- a/system/acl/deprecated.go +++ b/system/acl/deprecated.go @@ -1,4 +1,4 @@ -// Package acl exposes the internal/system/acl package. +// Package acl exposes the internal/acl package. // // Deprecated: This package will be removed in 0.4. package acl @@ -6,7 +6,7 @@ package acl import ( _ "unsafe" // for go:linkname - "hakurei.app/internal/system/acl" + "hakurei.app/internal/acl" ) type Perm = acl.Perm @@ -19,7 +19,7 @@ const ( // Update replaces ACL_USER entry with qualifier uid. // -//go:linkname Update hakurei.app/internal/system/acl.Update +//go:linkname Update hakurei.app/internal/acl.Update func Update(name string, uid int, perms ...Perm) error type Perms = acl.Perms diff --git a/system/dbus/deprecated.go b/system/dbus/deprecated.go index 67a309c2..8f8a6695 100644 --- a/system/dbus/deprecated.go +++ b/system/dbus/deprecated.go @@ -1,4 +1,4 @@ -// Package dbus exposes the internal/system/dbus package. +// Package dbus exposes the internal/dbus package. // // Deprecated: This package will be removed in 0.4. package dbus @@ -9,7 +9,7 @@ import ( _ "unsafe" // for go:linkname "hakurei.app/hst" - "hakurei.app/internal/system/dbus" + "hakurei.app/internal/dbus" "hakurei.app/message" ) @@ -17,13 +17,13 @@ type AddrEntry = dbus.AddrEntry // EqualAddrEntries returns whether two slices of [AddrEntry] are equal. // -//go:linkname EqualAddrEntries hakurei.app/internal/system/dbus.EqualAddrEntries +//go:linkname EqualAddrEntries hakurei.app/internal/dbus.EqualAddrEntries func EqualAddrEntries(entries, target []AddrEntry) bool // Parse parses D-Bus address according to // https://dbus.freedesktop.org/doc/dbus-specification.html#addresses // -//go:linkname Parse hakurei.app/internal/system/dbus.Parse +//go:linkname Parse hakurei.app/internal/dbus.Parse func Parse(addr []byte) ([]AddrEntry, error) type ParseError = dbus.ParseError @@ -46,12 +46,12 @@ type ProxyPair = dbus.ProxyPair // Args returns the xdg-dbus-proxy arguments equivalent of [hst.BusConfig]. // -//go:linkname Args hakurei.app/internal/system/dbus.Args +//go:linkname Args hakurei.app/internal/dbus.Args func Args(c *hst.BusConfig, bus ProxyPair) (args []string) // NewConfig returns the address of a new [hst.BusConfig] with optional defaults. // -//go:linkname NewConfig hakurei.app/internal/system/dbus.NewConfig +//go:linkname NewConfig hakurei.app/internal/dbus.NewConfig func NewConfig(id string, defaults, mpris bool) *hst.BusConfig const ( @@ -89,13 +89,13 @@ const ( // Address returns the session and system bus addresses copied from environment, // or appropriate fallback values if they are not set. // -//go:linkname Address hakurei.app/internal/system/dbus.Address +//go:linkname Address hakurei.app/internal/dbus.Address func Address() (session, system string) // ProxyName is the file name or path to the proxy program. // Overriding ProxyName will only affect Proxy instance created after the change. // -//go:linkname ProxyName hakurei.app/internal/system/dbus.ProxyName +//go:linkname ProxyName hakurei.app/internal/dbus.ProxyName var ProxyName string // Proxy holds the state of a xdg-dbus-proxy process, and should never be copied. @@ -106,10 +106,10 @@ type Final = dbus.Final // Finalise creates a checked argument writer for [Proxy]. // -//go:linkname Finalise hakurei.app/internal/system/dbus.Finalise +//go:linkname Finalise hakurei.app/internal/dbus.Finalise func Finalise(sessionBus, systemBus ProxyPair, session, system *hst.BusConfig) (final *Final, err error) // New returns a new instance of [Proxy]. // -//go:linkname New hakurei.app/internal/system/dbus.New +//go:linkname New hakurei.app/internal/dbus.New func New(ctx context.Context, msg message.Msg, final *Final, output io.Writer) *Proxy diff --git a/system/wayland/deprecated.go b/system/wayland/deprecated.go index bf364d9a..e83c9107 100644 --- a/system/wayland/deprecated.go +++ b/system/wayland/deprecated.go @@ -1,4 +1,4 @@ -// Package wayland exposes the internal/system/wayland package. +// Package wayland exposes the internal/wayland package. // // Deprecated: This package will be removed in 0.4. package wayland @@ -6,7 +6,7 @@ package wayland import ( _ "unsafe" // for go:linkname - "hakurei.app/internal/system/wayland" + "hakurei.app/internal/wayland" ) // Conn represents a connection to the wayland display server. -- cgit v1.3.1