From 9b507715d42f74c3f18fe817c2464abfca1b0182 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Wed, 8 Oct 2025 04:57:22 +0900 Subject: hst/dbus: validate interface strings This is relocated to hst to validate early. Signed-off-by: Ophestra --- hst/config.go | 7 ++++ hst/config_test.go | 4 ++ hst/dbus.go | 92 +++++++++++++++++++++++++++++++++++++++++++- hst/dbus_test.go | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 hst/dbus_test.go (limited to 'hst') diff --git a/hst/config.go b/hst/config.go index 7c96874f..9f4ee2f7 100644 --- a/hst/config.go +++ b/hst/config.go @@ -140,6 +140,13 @@ func (config *Config) Validate() error { Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"} } + if err := config.SessionBus.CheckInterfaces("session"); err != nil { + return err + } + if err := config.SystemBus.CheckInterfaces("system"); err != nil { + return err + } + if config.Container == nil { return &AppError{Step: "validate configuration", Err: ErrConfigNull, Msg: "configuration missing container state"} diff --git a/hst/config_test.go b/hst/config_test.go index ea50d4f1..eba2b793 100644 --- a/hst/config_test.go +++ b/hst/config_test.go @@ -20,6 +20,10 @@ func TestConfigValidate(t *testing.T) { Msg: "identity -1 out of range"}}, {"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds, Msg: "identity 10000 out of range"}}, + {"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}}, + &hst.BadInterfaceError{Interface: "", Segment: "session"}}, + {"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}}, + &hst.BadInterfaceError{Interface: "", Segment: "system"}}, {"container", &hst.Config{}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, Msg: "configuration missing container state"}}, {"home", &hst.Config{Container: &hst.ContainerConfig{}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull, diff --git a/hst/dbus.go b/hst/dbus.go index 7bad231e..6dd1a2c3 100644 --- a/hst/dbus.go +++ b/hst/dbus.go @@ -1,5 +1,27 @@ package hst +import ( + "strconv" + "strings" +) + +// BadInterfaceError is returned when Interface fails an undocumented check in xdg-dbus-proxy, +// which would have cause a silent failure. +type BadInterfaceError struct { + // Interface is the offending interface string. + Interface string + // Segment is passed through from the [BusConfig.CheckInterfaces] argument. + Segment string +} + +func (e *BadInterfaceError) Message() string { return e.Error() } +func (e *BadInterfaceError) Error() string { + if e == nil { + return "" + } + return "bad interface string " + strconv.Quote(e.Interface) + " in " + e.Segment + " bus configuration" +} + // BusConfig configures the xdg-dbus-proxy process. type BusConfig struct { // See set 'see' policy for NAME (--see=NAME) @@ -14,6 +36,74 @@ type BusConfig struct { // Broadcast set RULE for broadcasts from NAME (--broadcast=NAME=RULE) Broadcast map[string]string `json:"broadcast"` - Log bool `json:"log,omitempty"` + // Log turn on logging (--log) + Log bool `json:"log,omitempty"` + // Filter enable filtering (--filter) Filter bool `json:"filter"` } + +// Interfaces iterates over all interface strings specified in [BusConfig]. +func (c *BusConfig) Interfaces(yield func(string) bool) { + if c == nil { + return + } + + 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 + } + } +} + +// CheckInterfaces checks for invalid interface strings based on an undocumented check in xdg-dbus-error, +// returning [BadInterfaceError] if one is encountered. +func (c *BusConfig) CheckInterfaces(segment string) error { + if c == nil { + return nil + } + + 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 +} diff --git a/hst/dbus_test.go b/hst/dbus_test.go new file mode 100644 index 00000000..8b04494b --- /dev/null +++ b/hst/dbus_test.go @@ -0,0 +1,109 @@ +package hst_test + +import ( + "reflect" + "slices" + "testing" + + "hakurei.app/container" + "hakurei.app/hst" +) + +func TestBadInterfaceError(t *testing.T) { + testCases := []struct { + name string + err error + want string + }{ + {"nil", (*hst.BadInterfaceError)(nil), ""}, + {"session", &hst.BadInterfaceError{Interface: "\x00", Segment: "session"}, + `bad interface string "\x00" in session bus configuration`}, + {"system", &hst.BadInterfaceError{Interface: "\x01", Segment: "system"}, + `bad interface string "\x01" in system bus configuration`}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if gotError := tc.err.Error(); gotError != tc.want { + t.Errorf("Error: %s, want %s", gotError, tc.want) + } + if gotMessage, ok := container.GetErrorMessage(tc.err); !ok { + t.Error("GetErrorMessage: ok = false") + } else if gotMessage != tc.want { + t.Errorf("GetErrorMessage: %s, want %s", gotMessage, tc.want) + } + }) + } +} + +func TestBusConfigInterfaces(t *testing.T) { + testCases := []struct { + name string + c *hst.BusConfig + cutoff int + want []string + }{ + {"nil", nil, 0, nil}, + {"all", &hst.BusConfig{ + See: []string{"see"}, Talk: []string{"talk"}, Own: []string{"own"}, + Call: map[string]string{"call": "unreachable"}, + Broadcast: map[string]string{"broadcast": "unreachable"}, + }, 0, []string{"see", "talk", "own", "call", "broadcast"}}, + + {"all cutoff", &hst.BusConfig{ + See: []string{"see"}, Talk: []string{"talk"}, Own: []string{"own"}, + Call: map[string]string{"call": "unreachable"}, + Broadcast: map[string]string{"broadcast": "unreachable"}, + }, 3, []string{"see", "talk", "own"}}, + + {"cutoff see", &hst.BusConfig{See: []string{"see"}}, 1, []string{"see"}}, + {"cutoff talk", &hst.BusConfig{Talk: []string{"talk"}}, 1, []string{"talk"}}, + {"cutoff own", &hst.BusConfig{Own: []string{"own"}}, 1, []string{"own"}}, + {"cutoff call", &hst.BusConfig{Call: map[string]string{"call": "unreachable"}}, 1, []string{"call"}}, + {"cutoff broadcast", &hst.BusConfig{Broadcast: map[string]string{"broadcast": "unreachable"}}, 1, []string{"broadcast"}}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var got []string + if tc.cutoff > 0 { + var i int + got = make([]string, 0, tc.cutoff) + for v := range tc.c.Interfaces { + i++ + got = append(got, v) + if i == tc.cutoff { + break + } + } + } else { + got = slices.Collect(tc.c.Interfaces) + } + + if !slices.Equal(got, tc.want) { + t.Errorf("Interfaces: %q, want %q", got, tc.want) + } + }) + } +} + +func TestBusConfigCheckInterfaces(t *testing.T) { + testCases := []struct { + name string + c *hst.BusConfig + err error + }{ + {"nil", nil, nil}, + {"zero", &hst.BusConfig{See: []string{""}}, + &hst.BadInterfaceError{Interface: "", Segment: "zero"}}, + {"suffix", &hst.BusConfig{See: []string{".*"}}, + &hst.BadInterfaceError{Interface: ".*", Segment: "suffix"}}, + {"valid suffix", &hst.BusConfig{See: []string{"..*"}}, nil}, + {"valid", &hst.BusConfig{See: []string{"."}}, nil}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if err := tc.c.CheckInterfaces(tc.name); !reflect.DeepEqual(err, tc.err) { + t.Errorf("CheckInterfaces: error = %#v, want %#v", err, tc.err) + } + }) + } +} -- cgit v1.3.1