diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-07-02 21:52:07 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-07-02 21:52:07 +0900 |
| commit | 82561d62b66f17c05604e87f18187bb3a91f00d2 (patch) | |
| tree | c1543f85b4458b7d5cb2cf7b1baa9dee318debfe /system/dbus | |
| parent | eec021cc4b4eb42bc9c8311755826828bfba1996 (diff) | |
system: move system access packages
These packages loosely belong in the "system" package and "system" provides high level wrappers for all of them.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'system/dbus')
| -rw-r--r-- | system/dbus/address.go | 186 | ||||
| -rw-r--r-- | system/dbus/address_escape_test.go | 55 | ||||
| -rw-r--r-- | system/dbus/address_test.go | 119 | ||||
| -rw-r--r-- | system/dbus/config.go | 162 | ||||
| -rw-r--r-- | system/dbus/config_test.go | 159 | ||||
| -rw-r--r-- | system/dbus/dbus.go | 40 | ||||
| -rw-r--r-- | system/dbus/dbus_test.go | 213 | ||||
| -rw-r--r-- | system/dbus/export_test.go | 13 | ||||
| -rw-r--r-- | system/dbus/proc.go | 189 | ||||
| -rw-r--r-- | system/dbus/proxy.go | 117 | ||||
| -rw-r--r-- | system/dbus/samples_test.go | 228 | ||||
| -rw-r--r-- | system/dbus/stub_test.go | 9 | ||||
| -rw-r--r-- | system/dbus/testdata/dev.vencord.Vesktop.json | 18 | ||||
| -rw-r--r-- | system/dbus/testdata/org.chromium.Chromium+.json | 9 | ||||
| -rw-r--r-- | system/dbus/testdata/org.chromium.Chromium.json | 24 | ||||
| -rw-r--r-- | system/dbus/testdata/uk.gensokyo.CrashTestDummy.json | 21 |
16 files changed, 1562 insertions, 0 deletions
diff --git a/system/dbus/address.go b/system/dbus/address.go new file mode 100644 index 00000000..bec49e62 --- /dev/null +++ b/system/dbus/address.go @@ -0,0 +1,186 @@ +package dbus + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "slices" +) + +type AddrEntry struct { + Method string `json:"method"` + Values [][2]string `json:"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/system/dbus/address_escape_test.go b/system/dbus/address_escape_test.go new file mode 100644 index 00000000..3ea2cd26 --- /dev/null +++ b/system/dbus/address_escape_test.go @@ -0,0 +1,55 @@ +package dbus + +import ( + "testing" +) + +func TestUnescapeValue(t *testing.T) { + 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) { + 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/system/dbus/address_test.go b/system/dbus/address_test.go new file mode 100644 index 00000000..148bb092 --- /dev/null +++ b/system/dbus/address_test.go @@ -0,0 +1,119 @@ +package dbus_test + +import ( + "errors" + "reflect" + "testing" + + "git.gensokyo.uk/security/hakurei/system/dbus" +) + +func TestParse(t *testing.T) { + 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) { + 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/system/dbus/config.go b/system/dbus/config.go new file mode 100644 index 00000000..3f66b2bb --- /dev/null +++ b/system/dbus/config.go @@ -0,0 +1,162 @@ +package dbus + +import ( + "encoding/json" + "errors" + "io" + "os" + "strings" +) + +// ProxyPair is an upstream dbus address and a downstream socket path. +type ProxyPair [2]string + +type Config struct { + // See set 'see' policy for NAME (--see=NAME) + See []string `json:"see"` + // Talk set 'talk' policy for NAME (--talk=NAME) + Talk []string `json:"talk"` + // Own set 'own' policy for NAME (--own=NAME) + Own []string `json:"own"` + + // Call set RULE for calls on NAME (--call=NAME=RULE) + Call map[string]string `json:"call"` + // Broadcast set RULE for broadcasts from NAME (--broadcast=NAME=RULE) + Broadcast map[string]string `json:"broadcast"` + + Log bool `json:"log,omitempty"` + Filter bool `json:"filter"` +} + +func (c *Config) interfaces(yield func(string) bool) { + for _, iface := range c.See { + if !yield(iface) { + return + } + } + for _, iface := range c.Talk { + if !yield(iface) { + return + } + } + for _, iface := range c.Own { + if !yield(iface) { + return + } + } + + for iface := range c.Call { + if !yield(iface) { + return + } + } + for iface := range c.Broadcast { + if !yield(iface) { + return + } + } +} + +func (c *Config) checkInterfaces(segment string) error { + for iface := range c.interfaces { + /* + xdg-dbus-proxy fails without output when this condition is not met: + char *dot = strrchr (filter->interface, '.'); + if (dot != NULL) + { + *dot = 0; + if (strcmp (dot + 1, "*") != 0) + filter->member = g_strdup (dot + 1); + } + + trim ".*" since they are removed before searching for '.': + if (g_str_has_suffix (name, ".*")) + { + name[strlen (name) - 2] = 0; + wildcard = TRUE; + } + */ + if strings.IndexByte(strings.TrimSuffix(iface, ".*"), '.') == -1 { + return &BadInterfaceError{iface, segment} + } + } + return nil +} + +func (c *Config) Args(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 +} + +func (c *Config) Load(r io.Reader) error { return json.NewDecoder(r).Decode(&c) } + +// NewConfigFromFile opens the target config file at path and parses its contents into *Config. +func NewConfigFromFile(path string) (*Config, error) { + if f, err := os.Open(path); err != nil { + return nil, err + } else { + c := new(Config) + err1 := c.Load(f) + err = f.Close() + + return c, errors.Join(err1, err) + } +} + +// NewConfig returns a reference to a Config struct with optional defaults. +// If id is an empty string own defaults are omitted. +func NewConfig(id string, defaults, mpris bool) (c *Config) { + c = &Config{ + 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 +} diff --git a/system/dbus/config_test.go b/system/dbus/config_test.go new file mode 100644 index 00000000..f79c4c50 --- /dev/null +++ b/system/dbus/config_test.go @@ -0,0 +1,159 @@ +package dbus_test + +import ( + "errors" + "os" + "path" + "reflect" + "slices" + "strings" + "testing" + + "git.gensokyo.uk/security/hakurei/system/dbus" +) + +func TestConfig_Args(t *testing.T) { + for _, tc := range makeTestCases() { + if tc.wantErr { + // args does not check for nulls + continue + } + + t.Run("build arguments for "+tc.id, func(t *testing.T) { + if got := tc.c.Args(tc.bus); !slices.Equal(got, tc.want) { + t.Errorf("Args(%q) = %v, want %v", + tc.bus, + got, tc.want) + } + }) + } +} + +func TestNewConfigFromFile(t *testing.T) { + for _, tc := range makeTestCases() { + name := new(strings.Builder) + name.WriteString("parse configuration file for application ") + name.WriteString(tc.id) + if tc.wantErr { + name.WriteString(" with unexpected results") + } + + samplePath := path.Join("testdata", tc.id+".json") + + t.Run(name.String(), func(t *testing.T) { + got, err := dbus.NewConfigFromFile(samplePath) + if errors.Is(err, os.ErrNotExist) != tc.wantErrF { + t.Errorf("NewConfigFromFile(%q) error = %v, wantErrF %v", + samplePath, + err, tc.wantErrF) + return + } + + if tc.wantErrF { + return + } + + if !tc.wantErr && !reflect.DeepEqual(got, tc.c) { + t.Errorf("NewConfigFromFile(%q) got = %v, want %v", + samplePath, + got, tc.c) + } + if tc.wantErr && reflect.DeepEqual(got, tc.c) { + t.Errorf("NewConfigFromFile(%q) got = %v, wantErr %v", + samplePath, + got, tc.wantErr) + } + }) + } +} + +func TestNewConfig(t *testing.T) { + ids := [...]string{"org.chromium.Chromium", "dev.vencord.Vesktop"} + + type newTestCase struct { + id string + args [2]bool + want *dbus.Config + } + + // 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}, &dbus.Config{ + Call: make(map[string]string), + Broadcast: make(map[string]string), + Filter: true, + }}, + newTestCase{"", [2]bool{false, true}, &dbus.Config{ + Call: make(map[string]string), + Broadcast: make(map[string]string), + Filter: true, + }}, + newTestCase{"", [2]bool{true, false}, &dbus.Config{ + 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}, &dbus.Config{ + 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}, &dbus.Config{ + Call: make(map[string]string), + Broadcast: make(map[string]string), + Filter: true, + }}, + newTestCase{id, [2]bool{false, true}, &dbus.Config{ + Call: make(map[string]string), + Broadcast: make(map[string]string), + Filter: true, + }}, + newTestCase{id, [2]bool{true, false}, &dbus.Config{ + 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}, &dbus.Config{ + 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) { + 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/system/dbus/dbus.go b/system/dbus/dbus.go new file mode 100644 index 00000000..c5b4e812 --- /dev/null +++ b/system/dbus/dbus.go @@ -0,0 +1,40 @@ +// Package dbus wraps xdg-dbus-proxy and implements configuration and sandboxing of the underlying helper process. +package dbus + +import ( + "fmt" + "os" + "sync" +) + +const ( + SessionBusAddress = "DBUS_SESSION_BUS_ADDRESS" + SystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS" +) + +var ( + addresses [2]string + addressOnce sync.Once +) + +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 + addresses[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid()) + } else { + addresses[0] = addr + } + + // resolve upstream system bus address + if addr, ok := os.LookupEnv(SystemBusAddress); !ok { + // fall back to default hardcoded value + addresses[1] = "unix:path=/run/dbus/system_bus_socket" + } else { + addresses[1] = addr + } + }) + + return addresses[0], addresses[1] +} diff --git a/system/dbus/dbus_test.go b/system/dbus/dbus_test.go new file mode 100644 index 00000000..7b1e1858 --- /dev/null +++ b/system/dbus/dbus_test.go @@ -0,0 +1,213 @@ +package dbus_test + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "os/exec" + "strings" + "syscall" + "testing" + "time" + + "git.gensokyo.uk/security/hakurei" + "git.gensokyo.uk/security/hakurei/helper" + "git.gensokyo.uk/security/hakurei/internal" + "git.gensokyo.uk/security/hakurei/internal/hlog" + "git.gensokyo.uk/security/hakurei/system/dbus" +) + +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) { + oldWaitDelay := helper.WaitDelay + helper.WaitDelay = 16 * time.Second + t.Cleanup(func() { helper.WaitDelay = oldWaitDelay }) + + t.Run("sandbox", func(t *testing.T) { + proxyName := dbus.ProxyName + dbus.ProxyName = os.Args[0] + t.Cleanup(func() { dbus.ProxyName = proxyName }) + testProxyFinaliseStartWaitCloseString(t, true) + }) + t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) }) +} + +func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { + 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(), nil, nil) + } else { + p = dbus.New(t.Context(), 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(), 5*time.Second) + defer cancel() + if !useSandbox { + p = dbus.NewDirect(ctx, final, nil) + } else { + p = dbus.New(ctx, final, nil) + } + + p.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) { + return exec.CommandContext(ctx, os.Args[0], "-test.v", + "-test.run=TestHelperInit", "--", "init") + } + p.CmdF = func(v any) { + if useSandbox { + container := v.(*hakurei.Container) + if container.Args[0] != dbus.ProxyName { + panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0])) + } + container.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, container.Args[1:]...) + } else { + cmd := v.(*exec.Cmd) + if cmd.Args[0] != dbus.ProxyName { + panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0])) + } + cmd.Err = nil + cmd.Path = os.Args[0] + cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args[1:]...) + } + } + p.FilterF = func(v []byte) []byte { return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1] } + output := new(strings.Builder) + + t.Run("invalid wait", func(t *testing.T) { + wantErr := "dbus: not started" + if err := p.Wait(); err == nil || err.Error() != wantErr { + t.Errorf("Wait: error = %v, wantErr %v", + err, wantErr) + } + }) + + t.Run("string", func(t *testing.T) { + want := "(unused dbus proxy)" + if got := p.String(); got != want { + t.Errorf("String: %q, want %q", + got, want) + return + } + }) + + t.Run("start", func(t *testing.T) { + if err := p.Start(); err != nil { + t.Fatalf("Start: error = %v", + err) + } + + t.Run("string", func(t *testing.T) { + wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0]) + if useSandbox { + wantSubstr = fmt.Sprintf(`argv: ["%s" "-test.run=TestHelperStub" "--" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf`, os.Args[0]) + } + if got := p.String(); !strings.Contains(got, wantSubstr) { + t.Errorf("String: %q, want %q", + got, wantSubstr) + return + } + }) + + t.Run("wait", func(t *testing.T) { + done := make(chan struct{}) + go func() { + if err := p.Wait(); err != nil { + t.Errorf("Wait: error = %v\noutput: %s", + err, output.String()) + } + close(done) + }() + p.Close() + <-done + }) + }) + }) + } +} + +func TestHelperInit(t *testing.T) { + if len(os.Args) != 5 || os.Args[4] != "init" { + return + } + hakurei.SetOutput(hlog.Output{}) + hakurei.Init(hlog.Prepare, internal.InstallOutput) +} diff --git a/system/dbus/export_test.go b/system/dbus/export_test.go new file mode 100644 index 00000000..574a4a73 --- /dev/null +++ b/system/dbus/export_test.go @@ -0,0 +1,13 @@ +package dbus + +import ( + "context" + "io" +) + +// NewDirect returns a new instance of [Proxy] with its sandbox disabled. +func NewDirect(ctx context.Context, final *Final, output io.Writer) *Proxy { + p := New(ctx, final, output) + p.useSandbox = false + return p +} diff --git a/system/dbus/proc.go b/system/dbus/proc.go new file mode 100644 index 00000000..e8cccd5f --- /dev/null +++ b/system/dbus/proc.go @@ -0,0 +1,189 @@ +package dbus + +import ( + "context" + "errors" + "os" + "os/exec" + "path" + "path/filepath" + "slices" + "strconv" + "syscall" + + "git.gensokyo.uk/security/hakurei" + "git.gensokyo.uk/security/hakurei/helper" + "git.gensokyo.uk/security/hakurei/ldd" + "git.gensokyo.uk/security/hakurei/seccomp" +) + +// 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.CmdF != nil { + p.CmdF(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 { + toolPath := p.name + if filepath.Base(p.name) == p.name { + if s, err := exec.LookPath(p.name); err != nil { + return err + } else { + toolPath = s + } + } + + var libPaths []string + if entries, err := ldd.ExecFilter(ctx, p.CommandContext, p.FilterF, toolPath); err != nil { + return err + } else { + libPaths = ldd.Path(entries) + } + + p.helper = helper.New( + ctx, toolPath, + p.final, true, + argF, func(container *hakurei.Container) { + container.SeccompFlags |= seccomp.AllowMultiarch + container.SeccompPresets |= seccomp.PresetStrict + container.Hostname = "hakurei-dbus" + container.CommandContext = p.CommandContext + if p.output != nil { + container.Stdout, container.Stderr = p.output, p.output + } + + if p.CmdF != nil { + p.CmdF(container) + } + + // these lib paths are unpredictable, so mount them first so they cannot cover anything + for _, name := range libPaths { + container.Bind(name, name, 0) + } + + // upstream bus directories + upstreamPaths := make([]string, 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" || !path.IsAbs(pair[1]) { + continue + } + upstreamPaths = append(upstreamPaths, path.Dir(pair[1])) + } + } + } + slices.Sort(upstreamPaths) + upstreamPaths = slices.Compact(upstreamPaths) + for _, name := range upstreamPaths { + container.Bind(name, name, 0) + } + + // parent directories of bind paths + sockDirPaths := make([]string, 0, 2) + if d := path.Dir(p.final.Session[1]); path.IsAbs(d) { + sockDirPaths = append(sockDirPaths, d) + } + if d := path.Dir(p.final.System[1]); path.IsAbs(d) { + sockDirPaths = append(sockDirPaths, d) + } + slices.Sort(sockDirPaths) + sockDirPaths = slices.Compact(sockDirPaths) + for _, name := range sockDirPaths { + container.Bind(name, name, hakurei.BindWritable) + } + + // xdg-dbus-proxy bin path + binPath := path.Dir(toolPath) + container.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") + } + + errs := make([]error, 3) + + 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/system/dbus/proxy.go b/system/dbus/proxy.go new file mode 100644 index 00000000..cc0af1ea --- /dev/null +++ b/system/dbus/proxy.go @@ -0,0 +1,117 @@ +package dbus + +import ( + "context" + "fmt" + "io" + "os/exec" + "sync" + "syscall" + + "git.gensokyo.uk/security/hakurei/helper" +) + +// 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" + +type BadInterfaceError struct { + Interface string + Segment string +} + +func (e *BadInterfaceError) Error() string { + return fmt.Sprintf("bad interface string %q in %s bus configuration", e.Interface, e.Segment) +} + +// Proxy holds the state of a xdg-dbus-proxy process, and should never be copied. +type Proxy struct { + helper helper.Helper + ctx context.Context + + cancel context.CancelCauseFunc + cause func() error + + final *Final + output io.Writer + useSandbox bool + + name string + CmdF func(any) + + CommandContext func(ctx context.Context) (cmd *exec.Cmd) + FilterF func([]byte) []byte + + 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 *Config) (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, session.Args(sessionBus)...) + } + if system != nil { + if err = system.checkInterfaces("system"); err != nil { + return + } + args = append(args, system.Args(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, final *Final, output io.Writer) *Proxy { + return &Proxy{name: ProxyName, ctx: ctx, final: final, output: output, useSandbox: true} +} diff --git a/system/dbus/samples_test.go b/system/dbus/samples_test.go new file mode 100644 index 00000000..a5c6f273 --- /dev/null +++ b/system/dbus/samples_test.go @@ -0,0 +1,228 @@ +package dbus_test + +import ( + "sync" + + "git.gensokyo.uk/security/hakurei/system/dbus" +) + +const ( + sampleHostPath = "/tmp/bus" + sampleHostAddr = "unix:path=" + sampleHostPath + sampleBindPath = "/tmp/proxied_bus" +) + +var samples = []dbusTestCase{ + { + "org.chromium.Chromium", &dbus.Config{ + 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+", &dbus.Config{ + 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", &dbus.Config{ + 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", &dbus.Config{ + 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", &dbus.Config{ + 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 *dbus.Config + wantErr bool + wantErrF bool + bus [2]string + want []string +} + +var ( + testCasesV []dbusTestCase + testCasePairsV map[string][2]dbusTestCase + + testCaseOnce sync.Once +) + +func makeTestCases() []dbusTestCase { + testCaseOnce.Do(testCaseGenerate) + return testCasesV +} + +func testCasePairs() map[string][2]dbusTestCase { + testCaseOnce.Do(testCaseGenerate) + return testCasePairsV +} + +func injectNulls(t *[]string) { + f := make([]string, len(*t)) + for i := range f { + f[i] = "\x00" + (*t)[i] + "\x00" + } + *t = f +} + +func testCaseGenerate() { + // create null-injected test cases + testCasesV = make([]dbusTestCase, len(samples)*2) + for i := range samples { + testCasesV[i] = samples[i] + testCasesV[len(samples)+i] = samples[i] + testCasesV[len(samples)+i].c = new(dbus.Config) + *testCasesV[len(samples)+i].c = *samples[i].c + + // inject nulls + fi := &testCasesV[len(samples)+i] + fi.wantErr = true + + injectNulls(&fi.c.See) + injectNulls(&fi.c.Talk) + injectNulls(&fi.c.Own) + } + + // enumerate test case pairs + var pc int + for _, tc := range samples { + if tc.id != "" { + pc++ + } + } + testCasePairsV = make(map[string][2]dbusTestCase, pc) + for i, tc := range testCasesV { + 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(testCasesV) && testCasesV[i+1].id[len(testCasesV[i+1].id)-1] == '+' { + // attach system bus config + ftp[1] = testCasesV[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 + } + testCasePairsV[k] = ftp + } +} diff --git a/system/dbus/stub_test.go b/system/dbus/stub_test.go new file mode 100644 index 00000000..b05efd61 --- /dev/null +++ b/system/dbus/stub_test.go @@ -0,0 +1,9 @@ +package dbus_test + +import ( + "testing" + + "git.gensokyo.uk/security/hakurei/helper" +) + +func TestHelperStub(t *testing.T) { helper.InternalHelperStub() } diff --git a/system/dbus/testdata/dev.vencord.Vesktop.json b/system/dbus/testdata/dev.vencord.Vesktop.json new file mode 100644 index 00000000..05259774 --- /dev/null +++ b/system/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/system/dbus/testdata/org.chromium.Chromium+.json b/system/dbus/testdata/org.chromium.Chromium+.json new file mode 100644 index 00000000..0a27d719 --- /dev/null +++ b/system/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/system/dbus/testdata/org.chromium.Chromium.json b/system/dbus/testdata/org.chromium.Chromium.json new file mode 100644 index 00000000..be027f39 --- /dev/null +++ b/system/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/system/dbus/testdata/uk.gensokyo.CrashTestDummy.json b/system/dbus/testdata/uk.gensokyo.CrashTestDummy.json new file mode 100644 index 00000000..43272f67 --- /dev/null +++ b/system/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 +} |
