Harden context menu nil backend paths
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Snider 2026-04-17 20:01:58 +01:00
parent 4365248c86
commit f5cee5adaf
3 changed files with 45 additions and 1 deletions

View file

@ -1 +0,0 @@
- @bug pkg/contextmenu/service.go:42 — shutdown and menu mutation paths still assume a non-nil platform backend and can panic if the service is wired without one.

View file

@ -18,6 +18,10 @@ type Service struct {
registeredMenus map[string]ContextMenuDef
}
func platformUnavailableError(op string) error {
return coreerr.E("contextmenu."+op, "platform backend unavailable", nil)
}
func (s *Service) OnStartup(_ context.Context) core.Result {
s.Core().RegisterQuery(s.handleQuery)
s.Core().Action("contextmenu.add", func(_ context.Context, opts core.Options) core.Result {
@ -43,6 +47,10 @@ func (s *Service) OnShutdown(_ context.Context) core.Result {
// Destroy all registered menus on shutdown to release platform resources
s.mu.Lock()
defer s.mu.Unlock()
if s.platform == nil {
s.registeredMenus = make(map[string]ContextMenuDef)
return core.Result{OK: true}
}
for name := range s.registeredMenus {
_ = s.platform.Remove(name)
}
@ -90,6 +98,9 @@ func (s *Service) queryList() map[string]ContextMenuDef {
}
func (s *Service) taskAdd(t TaskAdd) error {
if s.platform == nil {
return platformUnavailableError("taskAdd")
}
s.mu.Lock()
defer s.mu.Unlock()
// If menu already exists, remove it first (replace semantics).
@ -129,6 +140,9 @@ func (s *Service) taskAdd(t TaskAdd) error {
}
func (s *Service) taskRemove(t TaskRemove) error {
if s.platform == nil {
return platformUnavailableError("taskRemove")
}
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.registeredMenus[t.Name]; !exists {
@ -145,6 +159,9 @@ func (s *Service) taskRemove(t TaskRemove) error {
}
func (s *Service) taskUpdate(t TaskUpdate) error {
if s.platform == nil {
return platformUnavailableError("taskUpdate")
}
s.mu.Lock()
defer s.mu.Unlock()
oldMenu, exists := s.registeredMenus[t.Name]
@ -182,6 +199,9 @@ func (s *Service) taskUpdate(t TaskUpdate) error {
}
func (s *Service) taskDestroy(t TaskDestroy) error {
if s.platform == nil {
return platformUnavailableError("taskDestroy")
}
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.registeredMenus[t.Name]; !exists {

View file

@ -126,6 +126,31 @@ func TestRegister_Good(t *testing.T) {
assert.NotNil(t, svc.platform)
}
func TestNilPlatform_Good_MutationAndShutdownAreSafe(t *testing.T) {
_, c := newTestContextMenuService(t, nil)
cases := []struct {
name string
action string
task any
}{
{name: "add", action: "contextmenu.add", task: TaskAdd{Name: "file-menu", Menu: ContextMenuDef{Name: "file-menu"}}},
{name: "remove", action: "contextmenu.remove", task: TaskRemove{Name: "file-menu"}},
{name: "update", action: "contextmenu.update", task: TaskUpdate{Name: "file-menu", Menu: ContextMenuDef{Name: "file-menu"}}},
{name: "destroy", action: "contextmenu.destroy", task: TaskDestroy{Name: "file-menu"}},
}
for _, tc := range cases {
r := taskRun(c, tc.action, tc.task)
assert.False(t, r.OK, tc.name)
err, _ := r.Value.(error)
require.Error(t, err)
assert.Contains(t, err.Error(), "platform backend unavailable")
}
assert.True(t, c.ServiceShutdown(t.Context()).OK)
}
func TestTaskAdd_Good(t *testing.T) {
mp := newMockPlatform()
_, c := newTestContextMenuService(t, mp)